4 min read

Defining Boundaries

Use Boundaries to define how teams share and collaborate on code.
Table of Contents

Boundaries are available on Enterprise plans

At the core of Boundaries are definitions. A definition is a set of rules that allow and restrict access to code.

You can define boundaries alongside packages, directories, and even modules (files).

Note that all examples use jsonc files for convenience. You can use also use json extensions if you prefer.

Boundaries has two access modes:

  • Public (default): All modules have access to other modules. Access is restricted by creating boundary definitions.
  • Private: All modules are restricted. Access is granted by creating boundary definitions.

For simplicity, this documentation will assume that you're using the default access mode.

Depending on your organization's needs, you can change the default access mode from public to private in boundaries.config.json. This file should be located at the workspace root.

boundaries.config.jsonc
{
  "defaultModuleAccess": "private",
}

Boundaries definitions work best when they're colocated with the code they're targeting, however you can also define boundaries in boundaries.config.json.

For any boundaries you define at this location, paths must be relative to the workspace root.

The following example defines a boundary restricting modules in the package foo so that they can only be imported by a middleware.ts module in a package in the apps directory

No other modules will be able to access the code in the foo package. Because the default access mode is public, no other code is affected by this definition.

boundaries.config.jsonc
{
  "definitions": [
    {
      "patterns": ["packages/foo/**"],
      "accessibleToPatterns": ["apps/*/src/middleware.ts"],
    },
  ],
}

Colocated boundary definitions are defined in boundaries.definitions.jsonc files.

There are two types of colocated boundaries that you can define:

  • Module boundaries: Define access for one or more modules. These are intended to be used to manage code within a package.
  • Package boundaries: Define access for a package's exports. These are intended to be used to manage access between packages.

A boundaries.definitions.jsonc file can be placed in any directory.

In this first example, we’re restricting access to some modules in a components folder. Anything matching those paths will only be able to used by modules within the app directory.

Note that all paths are relative to the boundaries.definitions.json file.

apps/dashboard/src/components/boundaries.definitions.json
[
  {
    "patterns": ["button/**", "link/**"],
    "accessibleToPatterns": ["../app/**"],
  },
]

Packages boundaries differ from module boundaries in that they define how other packages can access a package's exports (i.e. built code in a dist directory). The patterns property is replaced with exportPatterns, and the accessibleToPatterns property is replaced with accessibleToPackages.

In this example, the definition here controls access to the secret-keys.ts module that is exported from the auth package. Because this definition only lists the admin package as having access, no other packages will be able to make use of this export.

packages/auth/boundaries.definitions.json
[
  {
    "exportPatterns": ["dist/utils/secret-keys.ts"],
    "accessibleToPackages": ["admin"],
  },
]

Package boundaries also support restricting to specific directories within a package. In the next example, we're extending the previous example so that the definition now allows access to the secret-keys.ts module from:

  • Any module in the admin package.
  • Any modules in the src/pages/api directories of the dashboard and store-front packages.
packages/auth/boundaries.definitions.json
[
  {
    "exportPatterns": ["dist/utils/secret-keys.ts"],
    "accessibleToPackages": [
      "admin",
      {
        "packages": ["dashboard", "store-front"],
        "patterns": ["src/pages/api/**"],
      },
    ],
  },
]

Note that package names do not support glob syntax. They must be exact matches to the names of packages, as defined in each package's package.json file.

You can use the optional reason and reasonLink fields to provide information around boundaries, which will be displayed to users when they attempt to use restricted code.

If we take the previous example, where we were disallowing client-side code from accessing secret-keys.ts, we can make use of these fields to help users understand why this code is restricted:

packages/auth/boundaries.definitions.json
[
  {
    "exportPatterns": ["dist/utils/secret-keys.ts"],
    "accessibleToPackages": [
      "admin",
      {
        "packages": ["dashboard", "store-front"],
        "patterns": ["src/pages/api/**"],
      },
    ],
    "reason": "Importing from this module into bundled code, such as client-side application code, will cause secret keys to leak.",
    "reasonLink": "https://internal.docs/security/secret-keys",
  },
]

In some cases, it may be appropriate to include information like:

  • The team responsible for maintaining the restricted module(s).
  • How to contact the maintainers of the restricted module(s).
  • A link to a document explaining more information about why restrictions have been applied.

Boundaries supports glob patterns for all values, excluding package names. Some examples include:

  • ** any file, in any folder.
  • *.js any JS file, but only at the root.
  • **/*.css any CSS file, in any folder.
  • dist/**/*.{js,jsx} any JS or JSX file, in any folder, but only inside of dist.

The example below defines a boundary that allows the packages bar and baz, and only those packages, to import any module from the foo package.

packages/foo/boundaries.definitions.jsonc
[
  {
    "accessibleToPackages": ["bar", "baz"],
    "exportPatterns": ["**"],
  },
]
Last updated on February 21, 2024