Skip to content

Pacto for Platform Engineers

You manage the infrastructure that runs services. Pull a validated, machine-readable contract from an OCI registry and get everything needed to run a service: workload type, state model, interfaces, capabilities, dependencies and config schema. The contract states operational intent — what the service is, not how to deploy it — so how you provision, scale and wire it stays your decision.


What a contract tells you

The questions you'd normally ask the dev team — is it stateful, what does it expose, what does it depend on, what config does it need — are answered in the contract. The ones you answer yourself are not in it at all: there is no port, scaling, image or lifecycle field, because those are delivery decisions and the contract deliberately leaves them to you. The fields are top-level in v2, with no runtime wrapper:

Contract Field Platform Decision
workload (service / job / scheduled) Choose the workload kind — see Workload type below
state.type + state.persistence Choose storage + scheduling strategy — see State model below
state.dataCriticality: high Enable backups, stricter disruption budgets
interfaces[] (type + ref) Know the API surface — generate Service/Ingress wiring, publish the spec, drive conformance
interfaces[].visibility: public Create external Ingress or load balancer
capabilities[] (health / metrics binding) Configure liveness/readiness probes and metrics scraping from the bound interface + path
configurations[].schema / configurations[].ref Validate required configuration, generate config templates. Platform teams can publish a shared schema that services vendor into their bundles or reference via OCI — the schema then expresses what the platform provides. See Configuration Schema Ownership Models
policies[].ref Enforce organizational standards — require a health capability, enforce visibility rules, mandate an owner. See policies
readiness.claims[] Gate promotion and surface operational readiness — declare dashboard, runbook, security-review, SLO, AI-eval evidence; each claim carries a weight; the assessment carries a single expiry date; derive a readiness score. Enforce required claims via policies. See readiness
dependencies[].ref Validate dependency graph, check compatibility
docs/ (optional) Access service documentation, runbooks, integration guides
sbom/ (optional) Audit third-party packages, track license compliance

Your workflow

flowchart LR
    R[OCI Registry] --> PL[pacto pull]
    PL --> E[pacto explain]
    PL --> DI[pacto diff]
    PL --> G[pacto graph]
    PL --> GEN[pacto generate]
    GEN --> K[Deployment Artifacts]
    DI --> CI[CI Gate]

1. Pull a service contract

pacto pull oci://ghcr.io/acme/payments-api-pacto:2.1.0

explain, diff, graph and generate accept oci:// refs directly (resolving through the local cache), so this explicit pull is optional — use it only when you want the extracted bundle on disk.

A private repository needs credentials first: pacto login <registry>, or an already-authenticated gh for GHCR. Authentication gives the full resolution order.

2. Inspect it

$ pacto explain oci://ghcr.io/acme/payments-api-pacto:2.1.0
Service: payments-api@2.1.0
Owner: payments
Pacto Version: 2.0

Workload: service

State:
  Type: stateful
  Persistence: shared/persistent
  Data Criticality: high

Capabilities (2):
  - health
  - metrics

Interfaces (2):
  - rest-api (openapi: interfaces/openapi.yaml, public)
  - grpc-api (grpc: interfaces/service.yaml, internal)

Dependencies (1):
  - auth: oci://ghcr.io/acme/auth-pacto@sha256:abc123 (^2.0.0, required)

3. Check for breaking changes

pacto diff \
  oci://ghcr.io/acme/payments-api-pacto:2.0.0 \
  oci://ghcr.io/acme/payments-api-pacto:2.1.0

pacto diff exits non-zero if breaking changes are detected. Use the exit code in CI to gate deployments.

4. Resolve the dependency graph

$ pacto graph oci://ghcr.io/acme/payments-api-pacto:2.1.0
payments-api@2.1.0
├─ auth-service@2.3.0
  └─ user-store@1.0.0
└─ notifications@1.0.0 (shared)

Dependencies are resolved recursively from OCI registries. Sibling deps are fetched in parallel. Results are cached locally for fast repeated lookups.

Including config/policy references

By default, pacto graph shows only declared dependencies. To also visualize config/policy references — OCI refs in the configurations[].ref and policies[].ref fields — use the reference flags:

# Show dependencies AND config/policy references
pacto graph --with-references oci://ghcr.io/acme/payments-api-pacto:2.1.0

# Show ONLY config/policy references (no dependencies)
pacto graph --only-references oci://ghcr.io/acme/payments-api-pacto:2.1.0

References differ from dependencies: a dependency declares a runtime relationship between services (dependencies[].ref), while a reference points to a shared configuration or policy contract (configurations[].ref or policies[].ref). Both produce graph edges, but references are rendered with dashed lines in the dashboard graph.

5. Generate deployment artifacts

pacto generate <name> spawns a pacto-plugin-<name> binary and writes whatever it returns. Pacto ships no deployment-artifact plugin — the two official ones (schema-infer, openapi-infer) run inward, deriving contract inputs from files you already have:

pacto generate schema-infer ./payments-api --option file=config.yaml -o out/

Generating Helm charts or Kubernetes manifests means writing the plugin, which is deliberately small — a binary that reads a contract as JSON on stdin and writes file descriptions on stdout, in any language. Until one is on your PATH, pacto generate helm exits 1 with plugin "helm" not found. See the Plugin Development guide.


Mapping contracts to infrastructure

Workload type

workload Kubernetes resource Notes
service Deployment or StatefulSet Based on state.type
job Job Runs to completion
scheduled CronJob Schedule defined externally

State model

The scope/durability values below (e.g. shared/persistent) are shorthand for the state.persistence.scope + state.persistence.durability fields, matching the pacto explain display. These are platform-agnostic signals, not Kubernetes prescriptions — the mapping below is one reasonable interpretation for Kubernetes; the equivalent decision exists on Nomad, ECS or a custom platform:

state.type state.persistence Infrastructure
stateless local/ephemeral Deployment, no PVC, free to scale horizontally
stateful local/persistent StatefulSet + PVC, stable identity per replica
stateful local/ephemeral StatefulSet with emptyDir (stable identity, no durable storage)
stateful shared/persistent Network-attached or shared storage
hybrid local/persistent StatefulSet + PVC, tolerates cold starts
hybrid local/ephemeral Deployment with emptyDir, warm caches improve performance

Deployment mechanics the contract deliberately does not carry — upgrade strategy, graceful-shutdown timing, replica counts and autoscaling bounds — stay with your deployment tooling.


Configuration and policy

Two features give platform teams direct control over the boundary between developers and infrastructure: configuration schemas and policies.

Configurations: the interface between dev and platform

The configurations section defines the interface boundary between a service and its environment. When a platform team publishes a shared configuration schema, it declares what the platform provides — database connections, observability endpoints, feature flags, secret paths. When a service author defines one, it declares what the service requires.

You probably already have the schema. A service's configuration interface is the values.schema.json you author for its Helm chart; an infrastructure interface is a JSON Schema derived from the provisioning claim's OpenAPI schema. Either way you have two ways to attach it: vendor the file as a local schema: (required whenever you supply values), or resolve a schema-only contract via ref:.

Vendored: The platform publishes a schema externally, and services copy it into their bundle at build time:

configurations:
  - name: platform
    schema: configuration/platform-schema.json

Referenced (OCI): Services reference the platform's configuration contract directly. No vendoring required — Pacto resolves the schema from the referenced bundle at the fixed path configuration/schema.json:

configurations:
  - name: platform
    ref: oci://ghcr.io/acme/platform-config-pacto:1.0.0

See Configuration Schema Ownership Models for the full breakdown of service-defined vs. platform-defined schemas.

Policy: enforcing contract standards

The policies section lets platform teams enforce minimum requirements on contracts themselves. A policy is a JSON Schema that validates pacto.yaml — requiring a health capability, enforcing interface visibility rules, mandating a declared owner or a readiness gate, or any other organizational standard.

The platform team publishes a policy contract carrying the JSON Schema, and services adopt it by reference:

policies:
  - name: platform-policy
    ref: oci://ghcr.io/acme/platform-policy-pacto:1.0.0

See The platform-published policy + schema contract for the authoring and publish recipe.

Where refs are enforced

Ref-based policies are enforced by pacto validate and pacto push (fail-closed — an unresolvable ref is a hard POLICY_REF_UNRESOLVED error, which is how push blocks non-compliant publishes). pacto pack and the operator run local-only validation: they enforce only inline schema policies and emit a POLICY_REF_NOT_ENFORCED warning for refs.

See Layer 3: Policy enforcement for the resolution semantics (recursive N-hop, cycle detection, error codes) and policies for the full specification.

Info

Configuration and policy are complementary:

  • Configuration defines what a service needs (or what the platform provides) — the data interface
  • Policy enforces how contracts must be structured — the contract interface

Breaking change detection

pacto diff compares contract fields, deep-diffs referenced interface specs (e.g. OpenAPI) and resolves both dependency trees, so a change inside a dependency is classified alongside the service's own. That is the downward view. The blast radius — every consumer a change can reach — runs the other way and needs a fleet snapshot: that is pacto impact. Gate CI on the diff's exit code, but read what a non-zero exit does and does not mean first.

$ pacto diff oci://ghcr.io/acme/payments-api-pacto:1.0.0 \
             oci://ghcr.io/acme/payments-api-pacto:2.0.0
Classification: BREAKING
Changes (7):
  [NON_BREAKING] service.version (modified): service.version modified [1.0.0 -> 2.0.0]
  [BREAKING] state.type (modified): state.type modified [stateless -> stateful]
  [BREAKING] state.persistence.durability (modified): state.persistence.durability modified [ephemeral -> persistent]
  [POTENTIAL_BREAKING] dependencies.ref (modified): dependencies.ref modified [auth-service: oci://ghcr.io/acme/auth-service-pacto:1.5.0 -> auth-service: oci://ghcr.io/acme/auth-service-pacto:2.3.0]
  [POTENTIAL_BREAKING] dependencies.compatibility (modified): dependencies.compatibility modified [auth-service: ^1.5.0 -> auth-service: ^2.0.0]
  [BREAKING] dependencies (removed): dependencies removed [- redis]
  [BREAKING] interfaces (removed): interfaces removed [- grpc-api]

Dependency auth-service [NON_BREAKING] (1):
  [NON_BREAKING] service.version (modified): service.version modified [1.5.0 -> 2.3.0]

Dependency graph changes:
payments-api
├─ auth-service  1.5.0  2.3.0
└─ redis         -7.2.0
breaking changes detected

Read it in three parts. Changes is this contract's own diff, each row classified NON_BREAKING, POTENTIAL_BREAKING or BREAKING. A Dependency block appears for each dependency whose own contract changed, classified separately — auth-service only bumped its version, so nothing there is breaking. Dependency graph changes is the resolved closure: for a version change, - for a removal, + for an addition.

The headline Classification: is the worst classification anywhere in that output, dependency blocks included — so a diff whose own Changes rows are all non-breaking can still print BREAKING and exit 1 because a dependency's contract broke underneath it. A dependency is only diffed when it resolves in both trees: one that was added, removed or unreachable gets no block, and only shows up in the graph section. When both bundles include an sbom/ directory, package-level SBOM changes are reported but stay informational. See Change Classification Rules for the full table plus the OpenAPI, JSON-Schema and SBOM diff mechanics.


CI integration

Use Pacto in CI pipelines to catch problems before deployment:

# Example CI pipeline
# (Schema/OpenAPI inference is a service-authoring step — see developers.md)
steps:
  - name: Validate contract
    run: pacto validate .

  - name: Verify the lockfile is up to date
    run: pacto lock --check

  - name: Check for breaking changes
    run: pacto diff oci://ghcr.io/acme/my-service-pacto:latest .

  - name: Post diff as PR comment (markdown)
    run: |
      DIFF=$(pacto diff --output-format markdown oci://ghcr.io/acme/my-service-pacto:latest . 2>&1 || true)
      gh pr comment --body "$DIFF"

  - name: Verify dependency graph
    run: pacto graph .

pacto lock --check acts as a supply-chain reproducibility gate — it fails when a contributor edited dependencies or references without re-running pacto lock. See Lockfile.

Using GitHub Actions? See GitHub Actions integration for the equivalent workflow built on pacto-actions, including multi-service workflows, doc generation and authentication options.


Dashboard

pacto dashboard launches the operational dashboard — the same contracts the CLI manages and the operator verifies, organised around four workflows:

  • Overview — what needs attention right now, and how complete the data behind that answer is
  • Services — the inventory, with interfaces, configuration schemas and policy references per service
  • Operational Graph — dependency chains and where each revision actually runs, declared against observed
  • Change analysis — what changed between two revisions, and what that change affects

Sources (local, Kubernetes, OCI) are auto-detected at startup and merged per service. The platform-relevant behavior: when running alongside the Kubernetes operator, the dashboard auto-discovers OCI repositories from the resolvedRef fields in Pacto CRD statuses, so a K8s deployment gives the full contract experience — version history, interface details, configuration schemas and diffs — without explicit OCI arguments.

See Dashboard architecture for the source model, merge priority, graph edges and version-tracking rules, and the pacto dashboard command reference for its flags (--host, --port, --namespace, --diagnostics, --cors-origin, --traces, --trace-source) and environment variables. --no-cache works here too but is a global flag, not one of the command's own. Pass OCI repositories as positional oci:// arguments or via the PACTO_DASHBOARD_REPO env var.

Feeding the Operational Graph observed dependencies

The Operational Graph compares declared dependencies against observed ones, and the observed half comes from offline OTLP/JSON trace exports — files, not a live feed. Pacto ships no OTLP receiver and deploys no collector; if you run a Collector, you own it, and you point Pacto at whatever file it exports.

Ad hoc, pacto dashboard --trace-source orders=/path/traces.json names a source explicitly (--traces <file> still works and names sources by position). For the operator-managed dashboard, declare them in Helm values instead:

dashboard:
  observation:
    sources:
      - name: orders
        file: traces.json
        existingClaim: orders-trace-export

The operator mounts the claim read-only, reads only the file you declared, and exposes the source under the name you gave it. Storage lifecycle stays yours: Pacto reads, never writes, and never rotates.

The naming rules are load-bearing rather than cosmetic — name is the identity the API and UI use, it has to be unique against every other Data Source including k8s, local and oci, and a collision is refused before a snapshot is built. Observation sources states those rules, the read root each source is confined to, and why an unreadable source and a stale one are different answers; Observed dependencies and reconciliation is the model underneath.


Fleet queries without a browser

pacto fleet answers the same questions the dashboard's Operational Graph view answers, from a terminal or a CI job. It builds one snapshot from the sources you name — --local, --oci, --k8s, --evidence-url, --traces, --cache — then queries it with search, get, graph, status and explain:

$ pacto fleet search --local ./contracts
1 of 1 service(s):
  payments-api                 NotEvaluated owner=acme/payments  revs=1 targets=0

NotEvaluated is not a failure here: nothing has told the fleet where this service runs, so there is no operational target to evaluate it against. Add --k8s or an Evidence Server and the same service gets a compliance state per target.

Read the completeness before you read the rows. A degraded source is reported, never quietly dropped:

$ pacto fleet search --local ./contracts --oci ghcr.io/acme/unreachable-pacto:1.0.0
1 of 1 service(s):
  payments-api                 NotEvaluated owner=acme/payments  revs=1 targets=0
warning: answer is partial (as of 2026-08-23T01:30:40+02:00)
  - [SOURCE_PARTIAL] source oci returned a partial result
  - [SOURCE_RECORD_INVALID] ref ghcr.io/acme/unreachable-pacto:1.0.0 could not be resolved: artifact not found: ghcr.io/acme/unreachable-pacto:1.0.0

An unreachable registry never becomes an empty result, so a service missing from a partial answer may be one the missing source knew about. The header is equally deliberate: 1 of 1 is this page of total matches — search returns 100 rows unless you raise --limit (500 is the cap), so a bounded page can never be mistaken for the whole fleet. --output-format json carries the same facts in a meta envelope — completeness, limitations, per-source status — for a CI job to branch on.

Freshness and completeness has the full vocabulary, query semantics the five operations, and the pacto fleet reference every flag.


Tips

  • Build a plugin for your platform. A Helm plugin, Terraform plugin or custom manifest generator can consume Pacto contracts deterministically.
  • Use pacto graph to understand impact. Before upgrading a shared service, check what depends on it.
  • Disable cache in CI. Use --no-cache or PACTO_NO_CACHE=1 to ensure fresh OCI pulls in pipelines where the cache might be stale. --no-cache is a cold-start flag: it skips disk reads of pre-existing cached bundles, but bundles fetched during the run are still written to disk and reused within the same session.
  • Trust the state semantics. If a contract says stateless + ephemeral, you can safely use a Deployment with no PVC. The validation engine enforces consistency.
  • Use JSON output. Every inspection command (explain, diff, graph, validate, generate, doc) supports --output-format json for programmatic consumption.
  • Use markdown output for PR comments. pacto diff --output-format markdown renders changes as tables with old/new values — pipe it into gh pr comment for rich CI feedback.
  • Use --verbose for debugging. Pass -v to any command to see debug-level logs (OCI operations, resolution steps, cache hits/misses) on stderr.
  • Leverage AI assistants. Pacto contracts are machine-consumable. In addition to CI pipelines and platform controllers, AI assistants can interact with contracts directly through the MCP interface — useful for ad-hoc inspection, dependency analysis and contract generation.
  • Close the loop with the operator. The Kubernetes Operator is one runtime evidence source: it observes deployed workloads and reports whether they still match their contracts — workload alignment, state model, capability reachability, interface availability and more — as typed findings, never modifying your workloads. What it can evaluate depends on what you bind: an interface with no interfaceBindings entry has no port to check, so it reports Unknown rather than a violation. Combined with the dashboard, you get a complete view: contract truth from OCI + runtime truth from the operator.

See also