---
title: "Joints"
description: "What touches what — the twelve joint kinds, the mate that positions two parts, align, the `through` part that decides which way hardware goes in, and fastener patterns."
url: https://kaicad.barpom.xyz/docs/schema/joints
schema: "0.1"
source: packages/spec/src/design.ts
---

# Joints

A joint does three things from one declaration:

1. **It positions parts** — the mate places one anchor against another.
2. **It creates hardware** — the fixture becomes real items on the BOM.
3. **It cuts machining into both parts** — every hole, counterbore and countersink in a
   compiled design comes from a fixture's recipe.

Hardware can never be decorative. If there is a screw in the model, there is a pilot hole in
the receiving part and a clearance hole in the through part, and the validator has checked its
engagement and its edge distance.

```yaml
joints:
  - id: cleat_to_rail_left
    kind: screwed
    fixture: fixtures.wood_screw.4x50
    mate:
      a: slat_cleat@left:face.top
      b: side_rail@left:face.bottom
      align:
        contact: true
        offset: [0, -cleat_drop, 0]
    fasteners:
      pattern: { along: length, count: 10, spacing: auto, margin: 80 }
```

| Key | | |
| --- | --- | --- |
| `id` | required | Referenced by `outputs.drawings.details` and by findings. |
| `kind` | required | One of the twelve below. |
| `mate` | required | The two anchors, and how they align. |
| `fixture` | | A catalogue fixture id. Required for hardware kinds. |
| `through` | | Which part the fastener passes through. |
| `fasteners` | | `{ pattern: … }` — where the fasteners go. |
| `for_each` | | Expand once per named instance. |
| `array` | | Expand into a distributed run — see [Arrays](/docs/schema/arrays). |
| `glue` | | Whether the joint is glued. |
| `removable` | | Whether it comes apart. |
| `fit` | | `{ clearance: … }` — overrides the fixture's default. |

## The twelve kinds

| Kind | Fixture | |
| --- | --- | --- |
| `butt` | optional | Two parts simply touching |
| `glue_only` | no | Held by glue alone |
| `screwed` | required | A wood screw through one part into another |
| `doweled` | required | Fluted dowels, glued |
| `mortise_tenon` | no | Traditional joinery; the mortise is a feature |
| `dado` | no | One part seated in the other's trench |
| `rabbet` | no | One part seated in the other's L-cut |
| `bed_bolt` | required | Bolt into a cross dowel — knock-down |
| `cam_lock` | required | Flat-pack cam and dowel bolt |
| `threaded_insert` | required | Machine screw into an insert — knock-down |
| `pocket_screw` | required | Angled screw from a pocket |
| `bracket` | required | Metal bracket |

`kind` is what the assembly steps and the drawing call the joint. It does not change the
geometry — the **fixture** does that.

## `mate`

```yaml
mate:
  a: side_rail@left:face.start
  b: head_post@left:face.top
  align:
    contact: true
    roll: -90
    offset: [rail_drop_on_head_post, 0, 0]
```

`a` **moves**; `b` **is the target**. Both are
[anchor paths](/docs/schema/anchors#anchor-paths). The part on the `a` side is placed so that
its anchor meets the anchor on the `b` side.

Placement propagates outwards from the [ground part](/docs/schema/assembly-and-outputs). The
chain matters: ground the headboard panel, hang the posts off its ends, the rails off the
posts and the foot posts off the rails, and the whole piece stays symmetric about its centre
line for free.

### `align`

| Key | |
| --- | --- |
| `contact: true` | Make the two faces coincident and opposed. **The normal case.** |
| `offset: [x, y, z]` | A nudge, in the **target** anchor's local frame. |
| `roll: <degrees>` | Rotation about the mate normal, when the default is wrong. |
| `flip: true` | Flip the moving part end-for-end about the mate normal. |

**`offset` is local, not a position.** It is expressed in the frame of the anchor named in
`b`, which is what keeps it small and meaningful — "35 mm down the post's inner face" rather
than "at z = 265". If an offset in your file is a large number that looks like a coordinate,
the mate is probably attached to the wrong anchor.

```yaml
# Slide the cleat down the rail's inner face.
align:
  contact: true
  offset: [0, -cleat_drop, 0]
```

**`roll` in 90° steps is normal.** Each anchor carries a secondary axis so that one mate is a
complete placement, and the convention that sets it is mechanical rather than clever — see
[why an anchor carries a secondary axis](/docs/schema/anchors#why-an-anchor-carries-a-secondary-axis).
When a part arrives rotated about the mate normal, `roll` is the correction, and `roll: 180`
is the usual fix for a mirrored instance arriving upside down.

## `through` — which way the hardware goes in

```yaml
- id: rail_to_head_post_left
  kind: bed_bolt
  fixture: fixtures.bed_bolt.m8x150
  through: head_post@left
  mate:
    a: side_rail@left:face.start
    b: head_post@left:face.top
```

`through` names the part the fastener **passes through**. It defaults to the `a` side of the
mate, and it is stated explicitly whenever that is wrong.

These are two different questions. The mate says where the parts sit. `through` says which way
the hardware is driven in, and there is no reason the two should agree: a bed bolt goes
through the post from the outside into the end of the rail, whichever order the mate happened
to be written in. Tying them together would mean rewriting a working mate in order to fix a
hole.

The fixture then supplies the rest. Each entry declares a `through_from` — `mating` for a
screw that enters on the face touching the other part, `opposite` for a bed bolt whose
counterbore is on the outside face. Get `through` wrong and you put an 18 mm counterbore in
the wrong surface and run the clearance hole down the length of the board instead of across
it, which is exactly the kind of unbuildable design that still compiles and exports cleanly.

## `fasteners`

Where the fasteners go, as a [pattern](/docs/schema/features#patterns):

```yaml
fasteners:
  pattern: { along: length, count: 10, spacing: auto, margin: 80 }
```

```yaml
fasteners:
  pattern:
    along: length
    count: 2
    spacing: bolt_pitch
    from: rail_centre_height - bolt_pitch / 2
    to:   rail_centre_height + bolt_pitch / 2
```

Each position in the pattern becomes one fixture on the BOM and one full set of machining
operations on both parts. The fixture's recipe decides what those are; you never write a pilot
hole yourself.

## `glue`, `removable` and `fit`

```yaml
glue: false
removable: true
fit: { clearance: 0.3 }
```

`removable: true` is how a joint declares itself knock-down, and it is what
`requirements.knock_down` is checked against. `fit.clearance` overrides the fixture's
catalogue default for this joint only.

## `for_each`, and when not to use it

```yaml
- id: shelf_pins
  kind: butt
  for_each: [a, b, c]
  mate:
    a: shelf@{i}:face.bottom
    b: side@left:face.right
```

`{i}` is substituted with each name in turn. It is a genuine saving when the instances are
interchangeable.

It is **not** usable for mirrored `left`/`right` pairs. The inner face of the left post is its
`right` face and the inner face of the right post is its `left` face, and no substitution can
express that difference. Write those two joints out:

```yaml
- id: rail_to_head_post_left
  mate: { a: side_rail@left:face.start,  b: head_post@left:face.top }

- id: rail_to_head_post_right
  mate: { a: side_rail@right:face.start, b: head_post@right:face.bottom }
```

The duplication is honest. A `for_each` that quietly places one rail on the wrong face is not.

## `array`

A joint may expand into a distributed run of one part instead — slats, shelves, pickets. That
has its own page: [Arrays](/docs/schema/arrays).

## Worked: a bed bolt

```yaml
- id: rail_to_head_post_left
  kind: bed_bolt
  fixture: fixtures.bed_bolt.m8x150
  removable: true
  through: head_post@left
  mate:
    a: side_rail@left:face.start
    b: head_post@left:face.top
    align:
      contact: true
      roll: -90
      offset: [rail_drop_on_head_post, 0, 0]
  fasteners:
    pattern:
      along: length
      count: 2
      spacing: bolt_pitch
      from: rail_centre_height - bolt_pitch / 2
      to:   rail_centre_height + bolt_pitch / 2
```

That one declaration produces, per bolt: a 9 mm clearance hole and an 18 mm × 8 mm counterbore
through the post from its **outside** face, a 9 mm bore 100 mm into the end of the rail, and a
12.5 mm cross bore through the rail's width at 80 mm depth for the dowel to capture the bolt.
Ten operations across two parts from two lines of pattern, and every one of them checked.

The cross-dowel bore is the one that ruins knock-down beds when it lands in the wrong place,
and it is not a number anyone should be typing.
