Internal Packages

Purpose and Scope

Internal packages are libraries whose source code lives inside the same workspace as the applications that consume them. In Turborepo documentation, they are presented as one of the core concepts because they turn shared code into explicit package boundaries instead of scattered relative imports. A workspace package can provide user interface components, math utilities, TypeScript configuration, lint configuration, or any other reusable capability. The consuming application installs the package through the package manager workspace mechanism and then imports it by package name, which makes the relationship visible to both humans and Turborepo’s package graph.

Sources: apps/docs/content/docs/core-concepts/internal-packages.mdx, apps/docs/content/docs/core-concepts/index.mdx, apps/docs/content/docs/core-concepts/meta.json

Internal packages solve a practical monorepo problem: teams want shared code to be easy to find, easy to update, and safe to use from multiple apps. The core concept page defines them as libraries inside the workspace, while the repository-crafting guide describes them as building blocks that Turborepo can understand automatically through package dependency declarations. That means the important relationship is not only the import statement in source code; it is also the dependency entry in the consumer package manifest. Once that relationship is declared, Turborepo can optimize workflows around package relationships rather than treating the repository as unrelated folders.

Sources: apps/docs/content/docs/core-concepts/internal-packages.mdx, apps/docs/content/docs/crafting-your-repository/creating-an-internal-package.mdx

Relevant Source Files

  • apps/docs/content/docs/core-concepts/internal-packages.mdx: Defines Internal Packages, workspace dependency syntax, import usage, and the three compilation strategies: Just-in-Time Packages, Compiled Packages, and Publishable Packages.
  • apps/docs/content/docs/crafting-your-repository/creating-an-internal-package.mdx: Provides the task-oriented guide for creating a new package, including directory creation, package metadata, exports, scripts, and TypeScript-oriented compiled package setup.
  • apps/docs/content/blog/free-vercel-remote-cache.mdx: Explains why remote cache matters for repeated work across developers and CI, which is relevant when internal packages have build, type-check, or lint tasks that should not be recomputed unnecessarily.
  • apps/docs/content/blog/joining-vercel.mdx: Provides project context for Turborepo’s open-source CLI and Vercel-backed zero-configuration remote caching history, which affects how teams scale package workflows.
  • apps/docs/content/docs/core-concepts/index.mdx: Places Internal Packages within the core-concepts learning path alongside remote caching, package types, and package and task graphs.
  • apps/docs/content/docs/core-concepts/meta.json: Confirms the core-concepts section ordering that includes internal packages between package types and package-and-task-graph content.

Sources: apps/docs/content/docs/core-concepts/internal-packages.mdx, apps/docs/content/docs/crafting-your-repository/creating-an-internal-package.mdx, apps/docs/content/blog/free-vercel-remote-cache.mdx, apps/docs/content/blog/joining-vercel.mdx, apps/docs/content/docs/core-concepts/index.mdx, apps/docs/content/docs/core-concepts/meta.json

Core Model

The core model has three parts: a package, a consumer, and a declared dependency. The internal package has a package name, exports, and scripts. The consumer records that package as a dependency using workspace installation syntax, then imports the exported modules as if they came from an external registry package. The difference is ownership and resolution: the source stays in the repository, package manager workspaces connect the local package, and Turborepo can read the declared relationship when building the graph used for task scheduling and optimization.

The documentation shows different workspace dependency specifiers for common package managers. For pnpm and bun, the examples use the workspace protocol for an internal dependency such as a shared user interface package. For yarn and npm, the examples use a wildcard version in the consumer dependency entry. The conceptual point is the same across package managers: the consuming app should name the package in its manifest, rather than importing across folder boundaries without a package contract. This keeps shared code discoverable and makes dependency ownership visible during maintenance and review.

Sources: apps/docs/content/docs/core-concepts/internal-packages.mdx

Once the package is installed as a workspace dependency, application code imports from the package name. The Internal Packages page illustrates this with a button component imported from a shared user interface package and rendered inside a web page. That example is small, but it captures the desired mental model. Internal packages are consumed like external packages, even though they are developed locally. This consistency makes it easier to start with private shared code and later decide whether a package should become a publishable package for the npm registry.

Compilation Strategies

Turborepo’s documentation names three compilation strategies for internal packages. A Just-in-Time package exports source files directly and lets the consuming application’s bundler compile them. This can be the simplest option when applications use modern bundlers such as Turbopack, webpack, or Vite, and when the team is comfortable with application build time absorbing the package compilation work. The tradeoff is that the package itself may not produce a separate build artifact that Turborepo can cache independently for downstream consumers.

Compiled packages add a package-local build step, commonly with TypeScript or a bundler, and publish package exports that point to built output while retaining type information. The creating guide uses a math package example with build and development scripts and explicit exports for operations such as addition and subtraction. This pattern requires more setup than direct source exports, but it gives the package a clearer build boundary. That boundary is valuable because the package can have its own task lifecycle, outputs, and cacheable work inside the broader monorepo.

Sources: apps/docs/content/docs/core-concepts/internal-packages.mdx, apps/docs/content/docs/crafting-your-repository/creating-an-internal-package.mdx

Publishable packages are the most configured strategy because they must be prepared for consumers outside the workspace. The conceptual page frames publishing as an option rather than a requirement: an internal package can start as shared local code and later become an external package if a team needs registry distribution. This is an important design pressure when choosing exports, build output, and package metadata. A package intended only for internal use can optimize for workspace convenience, while a future publishable package should be stricter about public API shape and generated artifacts.

Creating a Package Flow

A typical creation flow begins by adding a directory under the workspace packages area, such as a new math utility package. The guide emphasizes that the package needs its own package manifest so that it becomes discoverable to the workspace and to Turborepo. The package name is not cosmetic; it determines the import path used elsewhere in the repository. Choosing a stable name early prevents churn in consuming applications and makes the package boundary explicit in dependency manifests, review diffs, and graph-based task planning.

After the package manifest is added, the package should define scripts that match its strategy. In the compiled package guide, development uses a watch mode and build uses TypeScript compilation. Exports describe the package’s public entry points, including type information and default JavaScript output. The package also declares development dependencies needed to build itself, such as a shared TypeScript configuration package and TypeScript. This keeps the internal package self-describing: another developer can inspect its manifest to understand how it is built, what it exposes, and what local tools it requires.

Sources: apps/docs/content/docs/crafting-your-repository/creating-an-internal-package.mdx

The consumer package then adds the new package to its dependencies and imports from the package name. This step is where Turborepo gains useful graph information. The relationship between the app and the package is no longer implicit in source layout; it is a declared dependency in the package manager’s model. Turborepo uses package relationships from manifests to create the package graph described by the guide, and that graph supports optimized repository workflows. In practice, this is why teams should resist reaching into another package’s source directory through long relative paths.

Caching and Workflow Implications

Internal packages often introduce repeated work: builds, type checks, lint checks, and application bundling may run locally and in continuous integration. The remote-cache blog explains Turborepo’s broader goal of avoiding the same work twice across developers and CI through a distributed cache. That matters for internal packages because a well-defined compiled package can make its work independently identifiable and reusable. If the package’s inputs do not change, developers and CI should benefit from cached task results instead of recompiling or rechecking the same package during every workflow.

Sources: apps/docs/content/blog/free-vercel-remote-cache.mdx, apps/docs/content/blog/joining-vercel.mdx

The Vercel-related blog posts provide context for scaling these workflows. One post states that commands using Turborepo on Vercel are automatically configured for Vercel Remote Cache, while other CI providers can authenticate using environment variables. Another earlier post notes that the CLI became open source and that Vercel provided zero-configuration remote caching. Those details are not specific to internal packages, but they explain why package boundaries and cacheable tasks are part of the same story: shared code becomes most powerful when repeated work around that code is coordinated across machines.

Practical Guidance

Choose the lightest strategy that still gives the repository the boundaries it needs. Just-in-Time packages are useful for fast-moving shared code when application bundlers are already able to compile the source and the team values minimal configuration. Compiled packages are a strong default when the package deserves its own build task or when downstream applications should consume generated output. Publishable packages are appropriate when the public contract must survive outside the monorepo. The documentation’s own learning path points readers from internal packages to package graphs and publishing guidance because the right choice depends on workflow and distribution needs.

For maintainability, treat an internal package like a real package from the beginning. Give it a clear name, define explicit exports, install dependencies where they are used, and make consuming applications depend on it through their manifests. This approach keeps the package graph meaningful, makes task dependencies easier to reason about, and gives Turborepo more useful information for scheduling and caching. After this page, read the creating guide to build a package step by step, then review package and task graph concepts to understand how those declarations influence execution order.

Sources: apps/docs/content/docs/core-concepts/internal-packages.mdx, apps/docs/content/docs/crafting-your-repository/creating-an-internal-package.mdx, apps/docs/content/docs/core-concepts/index.mdx