← Back to Blog
Header image for blog post: How to build efficient monorepo preview environments
Daniel Adeboye
Published 26th August 2026

How to build efficient monorepo preview environments

TL;DR: how to build efficient monorepo preview environments

  • Detect what changed: identify which services and shared packages were affected by the pull request.
  • Build only what needs rebuilding: use dependency relationships to determine which services require a new build.
  • Reuse unchanged services: avoid rebuilding services that were not affected, while keeping them available in the complete preview environment.
  • Deploy the full environment: use a preview blueprint to provision all the services, databases, and infrastructure required to test the application.
  • Clean up automatically: tear down preview environments when pull requests are merged or closed to control resource usage and costs.

Northflank lets you create full-stack preview environments for monorepos without rebuilding every service on every pull request. Build only affected services, provision the complete environment with databases and supporting services, and automatically tear it down when the PR is closed. Run it on Northflank's managed cloud or in your own AWS, GCP, Azure, or on-premises infrastructure. Get started with Northflank or book a demo.

Preview environments are straightforward when a repository contains a single application. A pull request changes the application, you build it, deploy it, and the team gets a URL. The build takes as long as it takes and the whole thing deploys, or it does not.

Monorepos make this harder. A repository with twelve services, four shared packages, and a handful of workers has a different problem: a PR that touches one service should not rebuild and redeploy eleven others. At three services this does not matter. At twelve it wastes ten minutes per PR. At thirty it makes preview environments unusable. The goal is a preview environment that is proportional to the change being tested.

Why monorepo preview environments are harder than single-repo

A single-repo preview environment has one question to answer: did the code change? Yes, so build and deploy it.

A monorepo preview environment has two additional questions. First, which services changed? You need to detect the changed files in the PR and map them to services. Second, which services are affected by those changes? A change to a shared authentication package does not change the authentication package's directory alone. It changes every service that imports that package. Without a dependency graph, you either rebuild everything (slow, expensive, misses the point) or rebuild only the directly modified service (fast, but potentially leaves other services running against a version of the shared package they are no longer compatible with).

Both questions require answers before you know what to build. Neither is present in single-repo preview environment implementations.

The three-layer problem

Before getting into implementation, it helps to separate three distinct problems that look like one problem.

Layer 1: What changed. This is a git question. Which files are different between the PR branch and the merge base? This produces a list of file paths.

Layer 2: What needs to rebuild. This is a dependency question. Given the changed files, which services need a new image built? Some services need a rebuild because their own source changed. Others need a rebuild because a shared package they depend on changed. This requires a dependency graph.

Layer 3: What needs to deploy. This is a preview environment question. The full preview environment needs all services running, including services that did not change and do not need a rebuild. Those services reuse their existing images. This is distinct from layer 2: you may rebuild two services but deploy all twelve.

Most implementations conflate these three layers and end up doing one of two things: rebuilding and redeploying everything (ignoring layers 1 and 2) or rebuilding only changed services and deploying an incomplete environment (handling layer 1 but not layer 3). Getting all three right is what makes monorepo preview environments both efficient and complete.

How path filters solve the direct change case

Deployment platforms like Northflank handle change detection through path filters: per-service configuration that defines which file paths should trigger a build for that service. When a PR opens, the platform checks which paths changed and triggers builds only for the services whose path filters match.

A typical monorepo layout:

monorepo/
├── apps/
│   ├── web/
│   └── api/
├── services/
│   ├── payments/
│   └── notifications/
└── packages/
    ├── auth/
    └── database/

Path filter configuration for the direct change case:

services:
  web:
    paths:
      - apps/web/**
  api:
    paths:
      - apps/api/**
  payments:
    paths:
      - services/payments/**
  notifications:
    paths:
      - services/notifications/**

A PR touching services/payments/src/routes.ts triggers a build for payments and nothing else. The platform handles the rest: detecting the changed paths, matching them against configured filters, triggering the right builds, and leaving everything else alone.

The shared package problem and how to solve it with path filters

Path filters for direct service changes are straightforward. The part that requires deliberate configuration is shared packages.

Consider packages/auth, imported by apps/apiapps/web, and services/payments. A change to packages/auth does not change any file inside those three service directories. With only direct path filters configured, none of them would trigger a rebuild. You would deploy a preview environment where those three services are running against the new version of packages/auth without having been rebuilt against it.

The fix is to include the shared package path in the path filters of every service that consumes it:

services:
  api:
    paths:
      - apps/api/**
      - packages/auth/**
      - packages/database/**
  web:
    paths:
      - apps/web/**
      - packages/auth/**
  payments:
    paths:
      - services/payments/**
      - packages/auth/**

Now a change to packages/auth triggers builds for apiweb, and payments , every service that imports it without touching services that do not.

Transitive dependencies: The cascade can go deeper. If packages/database is imported by packages/auth, and packages/auth is imported by apps/api, then a change to packages/database should trigger a rebuild of apps/api. You account for this by including all transitive dependency paths in each service's path filter:

services:
  api:
    paths:
      - apps/api/**
      - packages/auth/**        # direct dependency
      - packages/database/**    # dependency of auth

Global invalidation paths: Some changes should rebuild all services regardless of which service imports what. Lockfile changes (package-lock.jsonyarn.lockrequirements.txt) mean the installed versions of dependencies have changed, which can affect any service. A shared base Dockerfile change affects all images that use it. These paths go into every service's filter:

services:
  api:
    paths:
      - apps/api/**
      - packages/auth/**
      - packages/database/**
      - package-lock.json       # global invalidation
      - Dockerfile.base         # global invalidation

A useful reference for what triggers what:

ChangeServices to rebuild
apps/web/**web only
packages/auth/**all services that import auth
package-lock.jsonall Node.js services
Dockerfile.baseall services using that base image
docs/**README.mdnone

The preview environment blueprint for monorepos

A preview blueprint defines the complete set of services, databases, and configuration that compose a preview environment. In a monorepo, the blueprint covers all services, not just the ones that changed in a given PR.

When a PR opens:

  1. Change detection and dependency graph resolution produce the affected service set
  2. Affected services get new builds triggered; unchanged services skip the build step
  3. The preview blueprint deploys the full environment: new image tags for affected services, existing image tags (from previous builds) for unchanged services
  4. The environment is available at a unique URL

When a new commit is pushed to the same PR:

  1. The process repeats from step 1
  2. Only newly affected services rebuild
  3. The existing preview environment updates in-place: affected services redeploy with new images, unchanged services continue running

When the PR merges or closes, the entire environment tears down.

The blueprint approach means the preview environment is always complete: every service that the application needs is running, even if most of them did not change in this PR. A change to the payments service does not produce a preview environment with only the payments service. It produces a complete environment with all services, where the payments service has been updated.

Database isolation: The blueprint should provision isolated database instances for the preview environment, not connect to shared staging databases. Changes to the payments service may include schema migrations. Those migrations should run against an isolated fork of staging, not against the shared staging database that other open PRs are also using.

Service discovery within the preview environment: Services in the preview environment need to reach each other using the preview environment's internal URLs, not production or staging URLs. The blueprint handles this by injecting service URLs as environment variables at preview creation time. A frontend service that calls the API needs the API URL for this specific preview environment, not a hardcoded staging URL.

How Northflank handles monorepo preview environments

Northflank lets you combine per-service path filters, independent builds, and preview blueprints to create complete preview environments without rebuilding every service in a monorepo for every pull request.

Build only affected services: Each service can have its own path filters that define which changes should trigger a new build. These can include the service's own source directory, shared packages it depends on, and other files that should invalidate the build. When a pull request changes matching paths, Northflank can trigger a build for the affected service without triggering builds for unrelated services.

Build services independently: Services in the monorepo are built independently rather than through one monolithic build. This means a change to the API does not automatically require the frontend, worker, or other unrelated services to rebuild. It also makes it easier to apply different build configurations, caching strategies, and deployment settings to each service.

Deploy the complete preview environment: A preview blueprint defines the services, databases, networking, and other resources that make up the environment. When a pull request is opened, the blueprint creates the full environment while the build system determines which services need new builds. This separates what the preview environment contains from what needs to be rebuilt, so a small change does not require rebuilding the entire monorepo.

Fork databases for isolated previews: Database forking lets preview environments use isolated PostgreSQL, MySQL, and MongoDB instances. Database forks can be created from staging backups, giving each preview environment its own data layer for testing migrations and application changes without affecting shared environments.

Tear down previews automatically: When a pull request is merged or closed, its preview environment can be removed automatically. Teardown schedules and TTLs can also help clean up long-running previews and prevent unused environments from continuing to consume resources.

Run previews in your own cloud: BYOC allows preview environments, including their services and databases, to run inside your own cloud infrastructure. This gives teams the same monorepo preview workflow while retaining control over the underlying infrastructure.

For a practical walkthrough, see How to auto-create preview environments on every PR.

Conclusion

Monorepo preview environments work best when you separate what changed, what needs to be rebuilt, and what needs to be deployed. This lets you rebuild only affected services while keeping the complete preview environment available for testing.

Northflank provides the infrastructure for this workflow, with per-service builds, preview blueprints, managed database forking, automatic teardown, and BYOC for teams that want to run preview environments in their own cloud.

FAQs: how to build efficient monorepo preview environments

What is a monorepo preview environment?

A monorepo preview environment is an isolated environment created from a pull request in a repository containing multiple services or applications. It allows developers to test changes against the complete application without rebuilding every service in the monorepo.

How do you detect which services changed in a monorepo?

Compare the pull request with the merge base of its target branch, then map changed file paths to the services they belong to. Dependency graphs can then be used to identify additional services affected by changes to shared packages.

How do dependency graphs affect monorepo preview environments?

Dependency graphs show which services consume a changed package or service. A change to a shared package can therefore trigger builds for multiple services, even when those services have no direct source changes.

Should unchanged services be rebuilt in a preview environment?

Usually not. Unchanged services can reuse existing images or build artifacts while the services affected by the pull request receive new builds. The preview environment can still deploy the complete set of services.

How can you prevent duplicate builds in a monorepo?

Resolve all changed paths and dependency relationships into a single deduplicated set of affected services before creating the build matrix. This ensures each affected service is built once, even when multiple changes independently identify it.

Share this article with your network
X