Skip to content

Modules and workspaces

Buf works with directories of .proto files rather than individual file paths. You group those directories into modules. The Buf CLI operates on workspaces, which are collections of one or more modules configured together in a single buf.yaml. Almost every Buf command (buf build, buf lint, buf breaking, buf generate, buf push) takes a workspace as its input.

A module is a directory tree of .proto files that the Buf CLI and the BSR treat as a single unit. It’s the thing you build, version, publish, and depend on. Each module corresponds to one repository on the Buf Schema Registry, and within a module, imports resolve relative to the module root, never relative to the file doing the importing.

A workspace is where a project declares the configuration its modules share: common lint and breaking-change defaults, external BSR dependencies, and the ability for internal modules to import each other without any explicit dependency declaration. Even when you only have a single module, it lives in a workspace. Most buf.yaml files you’ll see in the wild are single-module workspaces.

Your first module

Most projects start with one module rooted at the top of a source-control repository. The smallest valid buf.yaml for that case is just the config version:

buf.yaml

yaml
version: v2

That form is equivalent to the more explicit one:

buf.yaml

yaml
version: v2
modules:
  - path: .

Both declare a single module rooted at the current directory and apply the default lint and breaking-change rule sets.

For a single-module workspace you can also specify a module name, rule overrides, and dependencies as top-level fields:

buf.yaml

yaml
version: v2
name: buf.build/acme/petapis
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE
deps:
  - buf.build/googleapis/googleapis

name is required if you want to publish the module to the BSR (see Module names and BSR repositories below), and optional otherwise.

This one-module, repo-rooted shortcut exists because it’s the common case. For any other layout (modules in a subdirectory, multiple modules, a workspace not at the repo root), declare an explicit modules list.

Multi-module workspaces

A workspace can hold any number of modules. Say you have two: acme/weatherapi/v1 under proto/, and units/v1 under vendor/. The layout looks like this:

sh
workspace_root
├── buf.gen.yaml
├── buf.lock
├── buf.yaml
├── proto
   └── acme
       └── weatherapi
           └── v1
               ├── api.proto
               ├── calculate.proto
               └── conversion.proto
├── vendor
   └── units
       └── v1
           ├── imperial.proto
           └── metric.proto
├── LICENSE
└── README.md

All Buf config files sit at the workspace root. The matching buf.yaml looks like this:

yaml
# Buf configuration version.
version: v2

# Modules included in the workspace. Each entry declares the directory
# containing its `.proto` files and, optionally, the module's BSR name,
# files or directories to exclude, and module-level lint and
# breaking-change settings.
#
# Paths and include/exclude patterns are relative to the workspace root.
modules:
  - path: proto
    name: buf.build/acme/weatherapi
  - path: vendor
    # A module's lint and breaking-change settings fully replace the
    # workspace defaults for that module. Rules are not merged.
    lint:
      use:
        - MINIMAL
    breaking:
      use:
        - PACKAGE

# Dependencies on modules outside the workspace. Shared by every module,
# and pinned to specific commits in the buf.lock file.
#
# Dependencies between modules in the workspace are resolved
# automatically, and should not appear here.
deps:
  - buf.build/googleapis/googleapis
  - buf.build/grpc/grpc

# Workspace-level lint and breaking-change settings, used as defaults
# for any module that doesn't override them.
lint:
  use:
    - STANDARD
breaking:
  use:
    - PACKAGE

Even in single-module workspaces, we recommend a directory and package structure that mirrors your Protobuf package names: proto/acme/weatherapi/v1/... rather than proto/*.proto at the top level. Using the same convention across projects makes it easier to pull other modules in as dependencies without import-path clashes. See Protobuf files and packages for the reasoning.

Module names and BSR repositories

A module’s name field is what ties it to a repository on the Buf Schema Registry. The format is:

Module name syntax

text
BSR_INSTANCE/ORGANIZATION/REPOSITORY

# Examples
buf.build/acme/petapis       # the public BSR at buf.build
example.buf.dev/acme/petapis # a Pro subscription on a custom subdomain
buf.example.com/acme/petapis # an Enterprise subscription on a custom domain
  • BSR_INSTANCE is the DNS name of the BSR. The public instance is buf.build. Pro and Enterprise customers have their own.
  • ORGANIZATION is a BSR organization.
  • REPOSITORY is the repository that holds every commit of the module.

name is optional while you’re working locally, but required to buf push the module: the name is how Buf knows where to push. It’s also what other Buf users put in their deps list to pull your module in as a dependency.

See the buf.yaml reference for the full list of configuration options, and the lint and breaking-change pages for their rule sets.

Unique file paths across modules

Buf requires every .proto file in a workspace to have a unique path relative to its module root. If you have two modules rooted at foo/ and bar/, you can’t have both foo/baz/baz.proto and bar/baz/baz.proto, because both resolve to the same logical import path baz/baz.proto.

Without that rule, an import "baz/baz.proto" statement anywhere in the workspace would be ambiguous:

bar/baz/bat.proto

protobuf
// This is the ambiguous case.
syntax = "proto3";

package bar.baz;

// which baz.proto is this, foo's or bar's?
import "baz/baz.proto";

With protoc, the answer depends on the order you pass -I flags, which isn’t visible from the file itself. The Buf CLI refuses to compile the workspace at all and tells you exactly where the collision is.

This situation comes up most often when you vendor third-party .proto files into a local directory.

Module README and LICENSE files

A module can include a README.md and a LICENSE file at its root. Both are treated as part of the module: when the BSR displays the module, it renders them alongside the Protobuf definitions.

README.md

The BSR renders the README.md as the module’s main documentation page. Changing the README creates a new commit, the same way that changing a .proto file does. CommonMark syntax is supported. See Document schemas for details.

LICENSE

Public modules on the public BSR are often open source. A license file is the standard way to declare how others are allowed to use, modify, and redistribute the code, so we encourage including one.

A LICENSE file (or a symlink to one) in the module directory is also required for the module’s Go package to be listed on pkg.go.dev; see the Go license policy.

Legal disclaimer

We aren’t lawyers. The BSR surfaces license information to help consumers make informed decisions, but we don’t guarantee its accuracy for any particular use. Consult a professional for legal questions about open source licensing.

How imports are resolved within a workspace

When a .proto file in the workspace imports another file, Buf resolves the import in two steps:

  1. Buf looks at every module in the current workspace. The import path is matched against the .proto files of each module, relative to that module’s path in buf.yaml. A file at proto/acme/weatherapi/v1/api.proto is imported as acme/weatherapi/v1/api.proto, no matter where the importing file lives.
  2. If no local module matches, Buf consults the workspace’s deps list and downloads the target module from the BSR.

Because every module in the workspace is visible to the local resolver first, modules in the same workspace can import each other without declaring a dependency on each other. A vendored .proto directory works the same way, as long as you declare it as a module.

See Dependency management for how deps, the buf.lock file, and the buf dep subcommands fit together.

Pushing a workspace to the BSR

buf push uploads the workspace to the BSR. Every module in the workspace that has a name field is pushed to its corresponding repository; modules without a name are skipped. Pushes go out in dependency order so that intra-workspace dependencies land correctly on the remote side.

See Pushing to the BSR for how buf push interacts with labels, commits, and CI setups.