---
title: "Appendix B: The contract, end to end"
canonical_url: https://ruslanakchurin.dev/blog/making-iac-boring/appendix-b
description: "One self-contained file implements the cross-tier contract: a producer publishes a typed value, a consumer names producer, key, context, and type, and a resolver returns the bound value or refuses by name."
datePublished: 2026-07-07
dateModified: 2026-07-07
series: making-iac-boring
seriesName: "Making IaC boring"
tags: ["infrastructure as code","typescript","typed contracts","reference implementation","node.js"]
about: [{"name":"Infrastructure as code","@type":"Thing","sameAs":"https://www.wikidata.org/wiki/Q24964334"}]
mentions: [{"name":"TypeScript","@type":"SoftwareApplication","sameAs":"https://www.wikidata.org/wiki/Q978185"},{"name":"Node.js","@type":"SoftwareApplication","sameAs":"https://www.wikidata.org/wiki/Q756100"}]
---
# Appendix B: The contract, end to end

_The resolution contract in one file._

Appendix A made the whole shape runnable. This one narrows to a single mechanism: the contract from [Resolve by contract](/blog/making-iac-boring/02-contract), enforced the way [Fail before apply](/blog/making-iac-boring/04-fail-before-apply) describes. One file: a producer publishes a typed value, a consumer names an address, and a resolver returns the bound value or refuses.

It is placeholder and self-contained. No provider SDKs, state backend, or network. The `catalogue` is an in-memory array standing in for stack outputs, a registry, or a generated manifest; that source is the implementation detail the contract hides.

## The type

The address locates a value; the type decides whether that value is the capability the consumer asked for. Names alone create accidental compatibility, so `NetworkRef` names the network identity and its attachable subnets, and a bare `string[]` cannot pass as one.

```ts path="contract.ts"
type Context = 'staging' | 'prod'

// A logical infrastructure type: the network identity and the
// subnets a workload may attach to, not the raw values behind them.
type NetworkRef = {
  readonly kind: 'NetworkRef'
  readonly vpcId: string
  readonly privateSubnetIds: readonly string[]
}

// The check that stops a string[] from resolving as a network.
function isNetworkRef(v: unknown): v is NetworkRef {
  return (
    typeof v === 'object' &&
    v !== null &&
    (v as NetworkRef).kind === 'NetworkRef' &&
    typeof (v as NetworkRef).vpcId === 'string' &&
    Array.isArray((v as NetworkRef).privateSubnetIds)
  )
}
```

## What the producer publishes

The producer owns the published values; where they live is not the consumer's concern. Each entry is keyed and scoped to a context, so the same producer and key can resolve in one context and be absent in another. It also records its `source`: the stack output, registry entry, or manifest key behind the address. The resolver names those sources when two collide on one address. `requires` records what a producer must resolve before it can publish — the edge the resolver walks to catch a cycle.

```ts path="contract.ts"
type Addr = { producer: string; key: string }

type Entry = Addr & {
  context: Context
  source: string // the stack output, registry entry, or manifest key
  requires?: Addr[] // must resolve before this entry can publish
  value: unknown // the backing value; its type is checked at resolve
}

// The environment producer publishes network.primary in two contexts.
// This array stands in for stack outputs, a registry, or a manifest.
const catalogue: Entry[] = [
  {
    producer: 'environment',
    key: 'network.primary',
    context: 'staging',
    source: 'env-stack',
    value: {
      kind: 'NetworkRef',
      vpcId: 'vpc-123',
      privateSubnetIds: ['subnet-a', 'subnet-b'],
    },
  },
  {
    producer: 'environment',
    key: 'network.primary',
    context: 'prod',
    source: 'env-stack',
    value: {
      kind: 'NetworkRef',
      vpcId: 'vpc-789',
      privateSubnetIds: ['subnet-x', 'subnet-y'],
    },
  },
]
```

## The consumer request

The consumer selects the address (a producer namespace and a key), the context it resolves in, and the type it expects back — not the backing value. It carries the type check with it, so a value that resolves as the wrong shape is refused before the consumer can use it.

```ts path="contract.ts"
type Request<T> = {
  producer: string
  key: string
  context: Context
  expects: string // type name, for the refusal message
  is: (v: unknown) => v is T // the type check itself
}

// workload-api asks the environment for its primary network in prod.
const request: Request<NetworkRef> = {
  producer: 'environment',
  key: 'network.primary',
  context: 'prod',
  expects: 'NetworkRef',
  is: isNetworkRef,
}
```

## The resolver

The resolver is the release-path refusal point. `locate` walks the address and the publish graph, and fails on the four that live there. `resolve` adds the type check. Every refusal names the part of the contract that failed before any provider call runs: the producer, plus the context on address failures, the key once the producer is known, and the expected type on the wrong-type refusal.

```ts path="contract.ts"
class ContractError extends Error {}

// Address and publish-graph failures: unknown producer, missing key,
// ambiguous binding, cycle.
function locate(
  cat: Entry[],
  producer: string,
  key: string,
  context: Context,
  path: string[],
): Entry {
  const at = `${producer}/${key}`

  if (path.includes(at)) {
    throw new ContractError(`cycle: ${[...path, at].join(' -> ')} in ${context}`)
  }

  const byProducer = cat.filter((e) => e.producer === producer)
  if (byProducer.length === 0) {
    throw new ContractError(
      `unknown producer: '${producer}' is not published in ${context}`,
    )
  }

  const matches = byProducer.filter(
    (e) => e.key === key && e.context === context,
  )
  if (matches.length === 0) {
    const elsewhere = byProducer.some((e) => e.key === key)
    throw new ContractError(
      elsewhere
        ? `missing key: '${at}' exists, but not in ${context}`
        : `missing key: '${at}' is not published`,
    )
  }
  if (matches.length > 1) {
    const sources = matches.map((e) => e.source).join(', ')
    throw new ContractError(
      `ambiguous binding: '${at}' in ${context} is published by ${matches.length} sources: ${sources}`,
    )
  }

  const [entry] = matches
  // The producer publishes only once its own dependencies resolve.
  for (const dep of entry.requires ?? []) {
    locate(cat, dep.producer, dep.key, context, [...path, at])
  }
  return entry
}

// Address failures first, then the type check.
function resolve<T>(cat: Entry[], req: Request<T>): T {
  const entry = locate(cat, req.producer, req.key, req.context, [])
  if (!req.is(entry.value)) {
    throw new ContractError(
      `wrong type: '${req.producer}/${req.key}' resolved, but not as ${req.expects}`,
    )
  }
  return entry.value
}
```

## Resolve, or refuse

A good composition resolves and the consumer proceeds. Then five compositions are wrong on purpose, and each stops at a named refusal before anything is applied.

```svg path="making-iac-boring/diagrams/appendix-b-refusals.diagram.ts" width="560"
resolver: request (producer / key / context / type)
resolved -> bound value -> apply proceeds
refused -> apply never runs:
  unknown producer
  missing key
  wrong type
  ambiguous binding
  cycle
```

```ts path="contract.ts"
// Resolves: apply may proceed.
const net = resolve(catalogue, request)
console.log(net.vpcId, net.privateSubnetIds)
//   vpc-789 [ 'subnet-x', 'subnet-y' ]

// refuse runs one bad composition and prints its refusal, so the file
// runs top to bottom instead of stopping at the first thrown error.
const refuse = (cat: Entry[], req: Request<NetworkRef>) => {
  try {
    resolve(cat, req)
  } catch (e) {
    console.log((e as Error).message)
  }
}

// Each of the following is refused before any provider call.

// unknown producer
refuse(catalogue, { ...request, producer: 'enviroment' })
//   unknown producer: 'enviroment' is not published in prod

// missing key: published in staging, not in prod
refuse([catalogue[0]], request)
//   missing key: 'environment/network.primary' exists, but not in prod

// wrong type: the value resolves, but it is a bare string[]
refuse(
  [
    {
      producer: 'environment',
      key: 'network.primary',
      context: 'prod',
      source: 'env-stack',
      value: ['subnet-x', 'subnet-y'],
    },
  ],
  request,
)
//   wrong type: 'environment/network.primary' resolved, but not as NetworkRef

// ambiguous binding: a copied stack kept the original producer identity,
// so two sources publish the same address with different values
refuse(
  [
    ...catalogue,
    {
      producer: 'environment',
      key: 'network.primary',
      context: 'prod',
      source: 'env-stack-copy',
      value: {
        kind: 'NetworkRef',
        vpcId: 'vpc-999',
        privateSubnetIds: ['subnet-p', 'subnet-q'],
      },
    },
  ],
  request,
)
//   ambiguous binding: 'environment/network.primary' in prod is published
//   by 2 sources: env-stack, env-stack-copy

// cycle: the environment cannot publish until a workload value resolves
refuse(
  [
    {
      producer: 'environment',
      key: 'network.primary',
      context: 'prod',
      source: 'env-stack',
      requires: [{ producer: 'workload-api', key: 'network-config' }],
      value: catalogue[1].value,
    },
    {
      producer: 'workload-api',
      key: 'network-config',
      context: 'prod',
      source: 'workload-api-stack',
      requires: [{ producer: 'environment', key: 'network.primary' }],
      value: null,
    },
  ],
  request,
)
//   cycle: environment/network.primary -> workload-api/network-config
//          -> environment/network.primary in prod
```

## Map it back to the series

- `NetworkRef` and `isNetworkRef` are the typed capability a producer publishes, from [Resolve by contract](/blog/making-iac-boring/02-contract). The type carries the semantic promise; the subnet IDs are only the backing values.
- `Request` names the address (a producer and a key), the context, and the expected type. The consumer selects the address, and resolution returns the value bound to it in the current context.
- `locate` and `resolve` are the resolver gate from [Fail before apply](/blog/making-iac-boring/04-fail-before-apply), and the five refusals are its five contract failures.
- The `catalogue` array is the transport, not the public contract: stack outputs, a registry, or a manifest, kept behind the address so the consumer never reads it directly.

This leaves out, on purpose, everything that is not the contract: the tier cut from [Start with the shape](/blog/making-iac-boring/01-shape), lifecycle membership from [Define tier membership](/blog/making-iac-boring/03-membership), the binding rules that govern shared-surface mutation in [Fail before apply](/blog/making-iac-boring/04-fail-before-apply), wave ordering, and the deletion order from [Re-cut the system](/blog/making-iac-boring/05-re-cut-the-system). Appendix A runs those together in the companion repository. This is only the seam where a consumer asks and the resolver answers or refuses.
