How-to › Test and mock integrations

How to publish and tag pacts in a Pact Broker#

Run the broker on Docker and Postgres, publish each pact under its commit and branch, and record deployments so the broker knows what runs where.

Audience
Platform team
Level
intermediate
Topic
Run contract and spec tests
Verified

The provider team verifies your contract against a three-week-old copy of the pact file committed in their repository. Your consumer test changed a field name on Tuesday, their verification kept running against the old copy, and production broke on Monday. Two repositories hold two different contracts, so neither team can say which one is current.

What you get

You will end up with a broker from one compose file, a consumer pipeline publishing every pact under its commit and branch, and a deployment record naming the commit each environment runs. This is for you if teams exchange pacts by hand.

Short answer

Run the Pact Broker image beside Postgres with PACT_BROKER_DATABASE_URL and PACT_BROKER_BASE_URL set, and give consumer CI the read-write credentials. Publish with pact-broker publish pacts --consumer-app-version "$GITHUB_SHA" --branch "$GITHUB_REF_NAME", so every build is a distinct version and the branch travels beside it. After each deploy, run record-deployment for that version and environment. The broker then answers which commit runs where, and refuses a changed contract under a reused version.

You will need

A CI runner that can run Docker, a Postgres the broker can own, the Pact Broker client CLI in the consumer pipeline, and one pact file from a consumer test. Verified 2026-10-21 against Node 22.22.0, Ruby 3.3.6, pact_broker 2.122.0, pact_broker-client 1.78.0, padrino-core 0.17.0, puma 6.6.1, and sqlite3 2.10.0. The sample runs the broker as the Ruby gem the Docker image wraps, over SQLite, because the check that re-runs this page has no Docker daemon. The compose file is committed and read, not run.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
Pact Broker (self-hosted)Several teams, a Postgres you already run, and a platform team willing to own one more serviceA container, a database, backups, TLS, and basic auth to operate, plus the upgrades the image publishesYou want single sign-on, per-team permissions, or bi-directional contracts without running anything
pact files committed to a shared git repositoryTwo teams, one provider, and a pact that changes rarelyEvery change is a pull request someone must merge in time, and the repository records what was merged rather than what each environment runsA third consumer appears, or anyone asks what is deployed where
PactFlowTeams, users, API tokens, and bi-directional contracts managed for you, driven by the same client CLIA paid hosted service, your contracts on someone else’s infrastructure, and a vendor’s roadmapContracts cannot leave your network, or the self-hosted broker already covers what you need

A git repository works while the number of pacts stays small, because every change is a pull request and the merge is the handshake. PactFlow removes the operating cost and takes the contracts with it, onto a hosted service with its own users and teams. The self-hosted broker sits between: one container, one database, and the CLI PactFlow also uses.

Run the broker against Postgres, with SQLite for the spike

The broker is one container reading two settings: where its database is, and the URL clients reach it at. The compose file pins the image and gives it a Postgres of its own.

  pact-broker:
    image: pactfoundation/pact-broker:3.0.0-pactbroker2.121.2
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "9292:9292"
    environment:
      PACT_BROKER_DATABASE_URL: postgres://pact:CHANGE_ME@postgres/pact_broker
      # The host the broker is reached at. Links in every response are built from it.
      PACT_BROKER_BASE_URL: https://pact.internal.example.com
      # Two basic auth users: one for the pipelines that publish, one that can only read.
      PACT_BROKER_BASIC_AUTH_USERNAME: ci
      PACT_BROKER_BASIC_AUTH_PASSWORD: CHANGE_ME
      PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME: reader
      PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD: CHANGE_ME
      PACT_BROKER_ALLOW_PUBLIC_READ: "false"

docker compose up -d starts both, and the broker migrates its own schema on first start. The image documentation describes the two basic auth users: read-write credentials for CI, read-only credentials for developers’ machines. Every setting is an environment variable named after the entry on the settings page with a PACT_BROKER_ prefix, which is where PACT_BROKER_ALLOW_PUBLIC_READ comes from.

The same page allows sqlite for investigations and spikes, which is how this page runs it. The image wraps a Ruby application, and the sample starts that application directly with a Rack file, so the gate can run it with no Docker daemon.

app = PactBroker::App.new do |config|
  config.database_connection = Sequel.connect(ENV.fetch("PACT_BROKER_DATABASE_URL"), encoding: "utf8")
  config.base_url = ENV["PACT_BROKER_BASE_URL"]
  # A fresh database is otherwise seeded with an example consumer, provider, and
  # deployments, which would show up beside your own.
  config.seed_example_data = false
end

Left at its default, a fresh broker carries an example consumer, an example provider, environments named test and production, and a recorded deployment of each.1

Publish under the commit and the branch

The consumer pipeline publishes after its own tests have written the pact. Two values go with the file: the version, which is the commit, and the branch, which is a property of that version rather than part of its name.

      - run: gem install pact_broker-client --version 1.78.0
      # The version is the commit, so every build on every branch is a distinct version and
      # the broker can say exactly which commit a pact, a verification, or a deployment
      # belongs to. The branch is recorded beside it, not as the version.
      - run: >
          pact-broker publish pacts
          --consumer-app-version "$GITHUB_SHA"
          --branch "$GITHUB_REF_NAME"
          --build-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"

The broker’s versioning guidance asks for exactly this: a version that is, or contains, the commit, and is unique.2 The client CLI reads the broker URL and the basic auth user from PACT_BROKER_BASE_URL, PACT_BROKER_USERNAME, and PACT_BROKER_PASSWORD, so the workflow sets those once as job environment variables. PACT_BROKER_TOKEN is PactFlow’s bearer token, which the self-hosted broker refuses with a 400. SCARF_ANALYTICS=false beside them stops pact-js reporting the consumer’s install to Scarf. A --tag flag still exists, and the tags page still states its rule: tag with the branch name on publish and the environment name on deploy. The same page says that branches and environments have superseded both uses.3 A pipeline built from scratch uses --branch and record-deployment, and adds --tag only for a provider whose Pact library still selects pacts by tag.

Record the deployment, so the broker knows what runs where

A pact in the broker says what a consumer version needs. Which consumer version is in production is a separate fact, and the broker only knows it if the deploy pipeline tells it. The recording page explains the two verbs: record-deployment for an application that replaces its previous version in an environment, and record-release for one that does not, such as a mobile app. The demo runs the whole sequence against a broker started on a free port.

node demo.mjs
== publish, commit 3f9c2d1 on main
Created billing-web version 3f9c2d1 with branch main
Pact successfully published for billing-web version 3f9c2d1 and provider billing-api.
  View the published pact at http://127.0.0.1:46861/pacts/provider/billing-api/consumer/billing-web/version/3f9c2d1

== create the production environment
Created production environment in the Pact Broker with UUID 24876d7d-911e-47b6-bb71-5c3adce83900

== record the deployment
Recorded deployment of billing-web version 3f9c2d1 to production environment in the Pact Broker.

== publish, commit 8b1e4f0 on main
Created billing-web version 8b1e4f0 with branch main
Pact successfully published for billing-web version 8b1e4f0 and provider billing-api.
  View the published pact at http://127.0.0.1:46861/pacts/provider/billing-api/consumer/billing-web/version/8b1e4f0

== what the broker knows
  version 8b1e4f0  branches: main
  version 3f9c2d1  branches: main
  production runs 3f9c2d1

The last three lines come from the broker’s API, not the CLI. Two versions sit on the main branch, and production runs the older one, because the second commit has been published but not deployed. The helper reads two HAL resources: /environments lists the production environment, and its currently-deployed resource lists the versions recorded against it.

export async function currentlyDeployed(baseUrl, pacticipant) {
  const { _embedded } = await hal(baseUrl, '/environments')
  const rows = []
  for (const environment of _embedded.environments) {
    const deployed = await hal(baseUrl, `/environments/${environment.uuid}/deployed-versions/currently-deployed`)
    for (const d of deployed._embedded.deployedVersions) {
      if (d._embedded.pacticipant.name === pacticipant) {
        rows.push({ environment: environment.name, version: d._embedded.version.number })
      }
    }
  }
  return rows
}

pacticipant is the broker’s word for either party to a pact.4 The deploy job in the committed workflow runs record-deployment after the deploy step, with the same $GITHUB_SHA the publish job used, which is the only reason the two records line up.

Check it worked

Six tests run against two brokers.

node --test
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 12347.975922

One publishes under the commit and reads the branch back from the version resource. One publishes the same content under the same version twice, which is accepted, then a changed contract under that version, which is refused. One records two deployments in turn and checks that the second replaces the first. One reads compose.yaml and checks the image, the Postgres URL, and the base URL are there. The last runs each workflow job’s credentials against a second broker with basic auth on. The summary line to compare against is # pass 6.

When it goes wrong

The deploy record says main is in production, but three commits have shipped from main since Monday. The pipeline published with --consumer-app-version main, so the branch name is the version, every build on the branch is the same version, and a deployment record of main names no commit. The pitfall script runs the same two builds that way.

node pitfall.mjs
== build 1 publishes as version main
Created billing-web version main

== deploy, recorded as version main
Recorded deployment of billing-web version main to production environment in the Pact Broker.

== which commit is live
  production runs main

== build 2 changes the contract and publishes as version main again, exit 1
Cannot change the content of the pact for billing-web version main and provider billing-api, as race conditions will cause unreliable results for can-i-deploy. Each pact must be published with a unique consumer version number. Some Pact libraries generate random data when a concrete value for a type matcher is not specified, and this can cause the contract to mutate - ensure you have given example values for all type matchers. For more information see https://docs.pact.io/go/versioning
@@ -15,6 +15,7 @@
         "body": [
           {
             "amount": 450,
+            "currency": "EUR",
             "customer": "Globex",
             "id": "in_1002"
           }

The first build is accepted. The second, which adds a field the consumer now reads, exits 1 with a diff of the two contracts, because a version holds one contract and main already has one. Change the pipeline to publish under $GITHUB_SHA with --branch main, as the workflow does, and both builds become distinct versions on one branch.

Every link the broker returns points at the wrong host, http://pact-broker:9292 inside the network instead of the address the pipeline used. PACT_BROKER_BASE_URL is unset, so the broker builds links from the request it received. Set it to the public URL, which the image documentation describes as strongly recommended in production because an unset base URL also exposes a cache poisoning weakness.

The broker that worked on a laptop answers 500 under two pipelines at once. It is running on SQLite, which the image documentation supports for investigations and spikes only, because it does not support concurrent requests. Point PACT_BROKER_DATABASE_URL at Postgres, as the compose file does.

When not to do this

Do not stand up a broker for one consumer and one provider who sit in the same team. A pact file in the provider repository, updated by pull request, is the whole workflow until a second consumer or a second environment arrives.

Do not record a deployment before the deploy has succeeded. The record is the fact the broker reasons from, and a record written at the start of a failed deploy leaves production described as running a version it never received.

Do not build a new pipeline on tags, which the broker’s own documentation marks as superseded by branches and environments.

Do not self-host to save money if nobody owns the service. The broker needs a database, backups, TLS, credentials, and image upgrades, and a broker nobody patches is an unauthenticated list of your internal APIs. PactFlow exists for the team that wants the workflow without the operations.

Do not keep SQLite past the spike. The database is disposed of with the container unless it is mounted, and the broker needs concurrent writers the moment two pipelines publish at once.

Last verified

Verified 2026-10-21 against Node 22.22.0, Ruby 3.3.6, pact_broker 2.122.0, pact_broker-client 1.78.0, padrino-core 0.17.0, puma 6.6.1, and sqlite3 2.10.0. Every output block is what the command preceding it printed, run in the page’s code directory after npm ci, which installs the gems through Bundler. The broker ran as the Ruby gem over SQLite; the Docker image, Postgres, and the GitHub Actions workflow were read from their documentation and not run.

Footnotes

  1. The default is in the broker’s own runtime configuration, where seed_example_data is set to true. On an empty database the broker creates two applications, Example App and Example API, the environments test and production, and a deployment of each into both. The first answer to the question this page is about is therefore an invented one. The gem’s README describes the same first visit for its example directory, a pact between the Zoo App and the Animal Service. ↩︎ Back to text

  2. The versioning page opens with a one-line summary: the version used to publish pacts and verification results should be, or contain, the commit, to avoid race conditions. The broker enforces the argument. The refusal in the pitfall gives the same two reasons, race conditions and can-i-deploy, and ends with a link back to the page that made them. ↩︎ Back to text

  3. The tags page states a Golden Rule for tagging: tag with the branch name when you publish and with the environment name when you deploy. Above it sits a tip saying that tags representing branches and environments have been superseded by first-class support for both. The rule outlives its own retirement as the migration path for clients that have not caught up. ↩︎ Back to text

  4. Consumer and provider applications are known as pacticipants, which the broker overview calls a bad pun the Pact Broker author now regrets. The word is in every CLI flag, --pacticipant, and every URL, /pacticipants/billing-web, so the regret has stayed in the prose. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.