The layout columns defaulted to '[]' — an empty array for a column whose only legal contents are a
LayoutNode object — and every upsert that omitted a layout wrote it. Creating a dashboard from the
dashboard list is exactly that path, so the key came back present, the client's `key in state` check
preferred it over the caller's default, and normalizeLayout called .children.map on it and threw.
Three layers, because none of them was enforcing anything:
- the columns are nullable with no default: NULL means "none stored", which is the truth
- getAllDashboardState omits the key when what is stored is not an object, so rows written before this
are repaired by the next write rather than crashing the read
- useDashboardState checks kind-compatibility before casting jsonb to T, and falls back to the caller's
default when it does not match. Only object-shaped defaults are guarded — a wrong primitive is a
cosmetic surprise, a wrong container is a crash.
Verified against the live DB: creating a dashboard with no layout no longer emits a ws-layout key, and
a row hand-set back to '[]' is omitted too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>