# Branded IDs at the wire — accept the loss, re-brand on parse

TypeScript branded types (`BeatId`, `TakeId`, `ProjectId`, `ProposalId`, etc.) live in `@recoil/contracts/branded.ts` and are enforced through the type system at compile time. Branding gives us "you cannot pass a `TakeId` where a `BeatId` is expected" — a real-world Law 2 / Law 6 win for the editor surfaces that thread these ids through dozens of components.

Branded types do NOT survive the HTTP boundary. JSON has no nominal type system: every id field on every wire shape collapses to `string` the moment it leaves the runtime. The Pydantic models on the API side carry their own validation (regex, length, uuid checks where applicable) but the brand information is gone by the time the bytes hit the network. This is a fundamental TypeScript limitation, not a project bug. The console-v2-audit-2026-05-04 (Cluster 7, "wire/runtime brand asymmetry") flagged it explicitly and recommended documenting the decision rather than trying to engineer around it.

Three options were considered and rejected:

1. **Brand at parse time via zod refinements.** Every `parse*Response` would re-stamp ids back into branded form via `as BeatId`-style assertions. Rejected for now — adds N call sites with no runtime safety improvement (the runtime already validated shape via zod). Re-instate this if a class of bugs surfaces from id-mixing across HTTP boundaries.
2. **Tag ids on the wire via `{ value, kind }` envelopes.** Rejected — breaks every existing route's response shape, doubles wire size, and Pydantic + zod schemas would still need the unwrap step on both sides. Pure tax for an edge-case bug class.
3. **Use UUID v5 with namespace per id-kind.** Rejected — adds engine-side complexity (every id minter learns its namespace) for runtime detection that the type system already gives us at compile time inside the TS process.

The decision is to ACCEPT the loss at the wire and re-brand only at specific high-risk call sites if the empirical bug rate justifies it. The branded types continue to do their job inside the TS process; the wire is treated as a known nominal-information sink.

Cited: console-v2-audit-2026-05-04 Cluster 7. See also ADR-0009 (codegen Pydantic to TypeScript) for the parallel reason zod schemas are generated, not hand-written. Phase 17 of the parent Console v2 build originally specced zod-refinement re-branding; this writeup supersedes that prior request and defers it to a CP-N+ build that has actual evidence of id-mixing bugs.
