# `blocks` JSON schema

This is the §8.1 specification for the ProseMirror serialisation format used
on the wire. The executable contract is `Blocks` / `BlocksV1` in
`libs/contracts/src/entities.ts` plus sanitiser `sanitizeMessageBlocks` in
`libs/core/src/blocks.ts`.

## Versioning

Every payload carries `v`. Today the only accepted value is `1`.

```
Blocks = discriminated union on `v`
  v = 1 → BlocksV1
```

Unknown `v` is rejected by the sanitiser and replaced with a plain-text
fallback document. There is no silent coercion of v2-shaped trees into v1.

A future `v = 2` is additive: a new union member, a new sanitiser branch, and
a migration that rewrites stored jsonb on read (lazy) rather than a table
rewrite. Stored rows keep the version they were written with until edited.

## BlocksV1

```
{
  "v": 1,
  "doc": ProseMirrorNode,          // required; type must be "doc"
  "forwardedMessage"?: Snapshot    // optional; trusted server-built preview
}
```

`doc.attachments` may also carry up to 10 claimed file ids (server-validated
against `attachments` rows owned by the author in that channel).

### Allowed node types

| type | role | attrs |
|---|---|---|
| `doc` | root | — |
| `paragraph` | block | — |
| `heading` | block | `level` 1–3 |
| `bulletList` / `orderedList` / `listItem` | block | `start` on orderedList |
| `blockquote` | block | — |
| `codeBlock` | block | — |
| `text` | inline | `text` string |
| `hardBreak` | inline | — |

Any other `type` is dropped. Nested content outside these types is dropped.

### Allowed marks

`bold`, `italic`, `underline`, `strike`, `code`, `link` (`attrs.href` only).

At most 8 marks per text node.

### Budgets (enforced in `sanitizeMessageBlocks`)

| Limit | Value |
|---|---|
| Max nodes | 2 000 |
| Max tree depth | 12 |
| Max text characters | 40 000 |
| Max attachments | 10 |

A document that exceeds a budget is truncated / replaced with the plain-text
fallback built from the message `text` field. The composer (TipTap, which is
ProseMirror) must stay inside these budgets; see SPIKES.md OD-2.

## Migration story

1. **Write path:** always sanitise. Never persist a client-supplied tree that
   failed sanitisation.
2. **Read path:** if `v` is missing or unknown, treat as plain text.
3. **v2:** add a union member; old clients on protocol 1.x never receive v2
   rows (server down-converts to v1 or to `text` until those clients upgrade).

The ProseMirror document is a permanent wire contract. Do not reuse field
names for a different meaning inside the same `v`.
