Contracts — Custom Hardware from typeCAD

When you design a custom board in typeCAD, a contract is a small JSON file that tells Cuttlefish exactly which pins on the chip are wired to what. Cuttlefish uses it to build a board definition that only exposes the pins your hardware actually uses — so you can’t accidentally write code for a pin that isn’t connected. Use this for any custom PCB (the circuit board), production board, or design that differs from a standard development board.

What’s in a Contract

A contract is a JSON file (.contract.json) exported from a typeCAD project. It captures:

  • Connected pins — which MCU (the main chip on the board) pins are wired to something on the PCB
  • Pin metadata — pin name, type (I/O, power, bidirectional), and net assignment
  • External components — what each pin connects to (resistors, capacitors, connectors, etc.)
  • Available peripherals — which peripheral buses (I2C, SPI, UART) are wired up

The transpiler reads this contract and generates a narrowed .cuttlefish/board.ts that exports only the pads the contract wires (plus the bus instances whose families the PCB routes). This means your TypeScript firmware can only import and use pins that are physically wired on your board — invalid pin access becomes a compile-time error.

With and Without a Contract

Without a ContractWith a Contract
Import any pin of the generated board moduleOnly connected pins are available
Hardware/software mismatch caught at runtimeMismatch caught at compile time
Must manually track which pins are wiredtypeCAD design is the source of truth
Works for standard dev boards like the ESP32-S3 DevKitCWorks for any custom PCB

Contracts are essential when working with custom hardware — custom PCBs, production boards, or any design where the MCU pins differ from a standard development board.

Contract File Format

A contract file is a JSON document with this structure:

{
  "version": 1,
  "mcu": {
    "symbol": "MCU_ST_STM32:STM32F411CEU6",
    "reference": "U1",
    "value": "",
    "footprint": "Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm"
  },
  "connectedPins": {
    "5": {
      "pinName": "PB6",
      "pinType": "bidirectional",
      "net": "net11",
      "externalComponents": [
        {
          "reference": "Y11",
          "dnp": false,
          "symbol": "Device:Crystal",
          "value": "",
          "footprint": "Crystal:Crystal_SMD_5032-2Pin_5.0x3.2mm"
        }
      ]
    },
    "6": {
      "pinName": "PB7",
      "pinType": "bidirectional",
      "net": "net12"
    }
  },
  "availablePeripherals": {
    "i2c": false,
    "spi": false,
    "uart": false
  }
}

Fields

FieldDescription
versionContract format version (currently 1)
mcu.symbolKiCad symbol library identifier for the MCU — the only mcu field Cuttlefish reads
connectedPinsMap of pin number to pin info — only pins wired on the PCB
connectedPins[n].pinNameFull pin name from the MCU symbol (may be compound like XTAL1/PB6)
connectedPins[n].pinTypePin direction from KiCad (e.g. bidirectional, input, output, power_in, passive)
connectedPins[n].boardNameOptional functional name from the typeCAD design (TX, LED); when it is a pad name it is used directly, otherwise it becomes an alias for the pad
connectedPins[n].netNet name the pin is connected to
connectedPins[n].externalComponentsOptional list of components on the net — each needs at least reference and dnp
availablePeripheralsWhich peripheral buses have all their required pins wired (i2c, spi, uart)

Pin Name Matching

Cuttlefish resolves each wired pin to a datasheet pad name: a pad-form boardName (PA9, P0.28, GPIO9, GP25) is used directly; otherwise the KiCad pinName is split on / and - and each segment checked for a pad-form name (XTAL1/PB6 resolves to PB6). Pins whose type or net marks them as power/ground (power_in, passive, nets like VCC/GND) are excluded — they are never exported as GPIO. A boardName that is not a pad name rides along as a functional alias (TXPA9).

Integrating with a Cuttlefish Project

Add soc and contract fields to your cuttlefish.config.ts:

import type { CuttlefishConfig } from '@typecad/cuttlefish/api';

const config: CuttlefishConfig = {
  entry: './src/main.ts',
  target: 'stm32f411',
  soc: 'stm32f411xe',
  contract: './src/my-board.contract.json',
  framework: '@typecad/framework-zephyr',
  output: {
    outDir: './out',
  },
  toolchain: {
    type: 'west',
  },
};

export default config;

The key differences from a standard board-based config:

  • soc instead of board — the Zephyr SoC name (e.g. 'stm32f411xe')
  • contract — path to the .contract.json file
  • No board field — board and contract are mutually exclusive

Both soc and contract are required for custom hardware (plus a framework — contract board generation requires @typecad/framework-zephyr); the transpile fails with an error naming the missing key when either is absent, and there is no auto-discovery of contract files.

⚙️ Advanced details — what the contract generates

What Gets Generated

When a contract is present, the transpiler generates two files. .cuttlefish/board.json is the full board manifest for the SoC (the same artifact a board-target project carries — the transpiler’s pin map and chip resolution read it), and .cuttlefish/board.ts is the narrowed module re-exported as @typecad/board:

// Excerpt of the generated .cuttlefish/board.ts — auto-generated, do not edit.
import { Pin } from '@typecad/hal';

// Narrowed pin set — only pins the contract declares connected.
export const PB6 = Pin.fromPort('PB6');
export const PB7 = Pin.fromPort('PB7');

// A bus family the PCB routes gets its instance — e.g. when
// availablePeripherals.i2c is true, `export const I2C0 = new I2CBus('I2C0');`

Your firmware imports the wired pads from the virtual @typecad/board module and hardware classes from @typecad/hal: import { GPIO } from '@typecad/hal'; import { PB6, PB7 } from '@typecad/board';, then const xtal1 = new GPIO(PB6, GPIO.OUTPUT);.

cuttlefish-env.d.ts

The transpiler also generates a .cuttlefish/cuttlefish-env.d.ts file that declares the @typecad/board ambient module. When using a contract, it points to the generated board:

declare module '@typecad/board' {
  export * from './board.js';
}

typeCAD Integration

Exporting a Contract from typeCAD

A contract file is generated from your typeCAD PCB project. When you define your hardware in typeCAD (fragment — mcu is your project’s MCU component):

import { PCB } from '@typecad/typecad';
import { Resistor, LED } from '@typecad/passives/0603';

let typecad = new PCB('my-board');
let r1 = new Resistor({ value: '330' });
let d1 = new LED();

typecad.net(mcu.PB5, r1.pin(1));
typecad.net(r1.pin(2), d1.pin(2));

typecad.create(r1, d1);

The typeCAD tooling can export a .contract.json that describes which MCU pins are connected and what they’re connected to. Place this file in your Cuttlefish project’s src/ directory.

Typical Project Setup

For a project that uses both typeCAD (hardware) and Cuttlefish (firmware), the recommended structure is:

my-project/
  hardware/                  # typeCAD project
    src/
      board.ts               # PCB design
    kiCAD/
      my-board.kicad_sch     # Generated schematic
    package.json
  firmware/                  # Cuttlefish project
    src/
      main.ts                # Firmware code
      my-board.contract.json # Exported from hardware/
    cuttlefish.config.ts
    package.json

Workflow

  1. Design hardware in typeCAD — define the schematic, place components, wire nets
  2. Export contract — generate a .contract.json from the typeCAD project
  3. Write firmware in Cuttlefish — the contract ensures you only use physically connected pins
  4. Transpile and compilecuttlefish build --compile --upload

Board vs Contract

The board and contract config fields are mutually exclusive:

ScenarioConfig
Standard dev board (ESP32-S3 DevKitC, XIAO nRF52840)board: 'esp32s3_devkitc/esp32s3/procpu'
Custom PCB designed in typeCADsoc: 'stm32f411xe' + contract: './src/board.contract.json'

Troubleshooting

“A ‘contract’ config requires a ‘soc’ …”

The contract narrows against a SoC, named by soc: in your cuttlefish.config.ts (e.g. 'stm32f411xe'). Add it alongside contract:.

“Framework ’…’ provides no contract board generation”

Contract board generation derives the SoC’s bus controllers from the installed Zephyr tree, so it requires framework: '@typecad/framework-zephyr' (and a Zephyr tree — install one with the framework’s zephyr-installer or set ZEPHYR_BASE).

“Specifying both ‘board’ and ‘contract’ is not allowed”

Remove one from your cuttlefish.config.ts. Use board for standard development boards, or soc + contract for custom hardware.

Contract file not found

contract: is a path resolved relative to the project root (the directory holding cuttlefish.config.ts) — there are no CLI flags and no auto-discovery. Check the path and that the file is valid JSON with "version": 1.