Developers
Document model
The Desyn document model — a frozen, versioned contract shared by the Rust engine, TypeScript, Convex, every client and the AI. Primitives, the Design object and invariants.
On this page
Every package that touches a design follows one contract, docs/DOCUMENT_MODEL.md. The type excerpts on these pages are rendered from that file when the docs are built, so they always match it exactly.
Key properties:
- Flat and addressable. A design is a flat map of nodes keyed by id; the tree is expressed with
parentand orderedchildren. Any node can be read or patched directly. - JSON, camelCase. The TypeScript definitions are the source of truth;
@desyn/schemaderives zod and JSON Schema from them anddesyn-coremirrors them with serde. - Canonical. A canonical JSON form (sorted keys, normalized numbers) is used for hashing, conformance fixtures and comparisons.
- Versioned.
schema: 1. Changes are additive and recorded in the contract's change log.
Primitives#
type Id = string; // 12 chars [a-z0-9], generated client-side (nanoid alphabet lowercase+digits). "root" is reserved.
type Color = string; // "#RRGGBB" or "#RRGGBBAA" (lowercase hex). No named colors, no rgb().
type Unit = "px" | "mm" | "in" | "pt";
type BlendMode = "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn"
| "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity";All geometry is in design pixels (1 px = 1/96 in) regardless of Page.unit (unit is for display / print export only).
Node x,y are relative to the parent's top-left (before parent rotation). rotation is degrees clockwise about the node
center. Origin top-left, y grows downward.
Design#
The top-level object. nodes["root"] is the root node; its children are the page ids, in order.
interface Design {
schema: 1;
id: Id;
name: string;
kind: DesignKind;
nodes: Record<Id, Node>; // flat map. nodes["root"] is the RootNode whose children are the Page ids, in order.
assets: Record<Id, AssetRef>; // assets referenced by nodes (images, svg, fonts, video, audio)
styles: DesignStyles; // reusable named styles + variables
brandKitId?: string; // Convex id of the brand kit this design follows (informational)
meta: { // free-form, never interpreted by the engine
createdAt?: number; updatedAt?: number; templateId?: string; tags?: string[]; locale?: string;
[k: string]: unknown;
};
}
type DesignKind =
| "social-post" | "story" | "poster" | "flyer" | "thumbnail" | "banner" | "ad" | "presentation" | "infographic"
| "business-card" | "resume" | "invitation" | "menu" | "logo" | "newsletter" | "document" | "whiteboard"
| "website" | "video" | "book-cover" | "certificate" | "sticker" | "mockup" | "custom";
interface AssetRef {
id: Id;
kind: "image" | "svg" | "font" | "video" | "audio" | "lottie" | "data";
url?: string; // resolvable URL (signed or public) - filled by clients at load time, never persisted by Convex
storageKey?: string; // S3 object key (server-side truth)
mime: string; bytes?: number; width?: number; height?: number; durationMs?: number;
hash?: string; // sha256 hex of the bytes (dedupe)
name?: string; source?: "upload" | "ai" | "stock" | "template" | "brand";
attribution?: { author?: string; license?: string; url?: string };
ai?: { provider: string; model: string; prompt: string; seed?: number };
family?: string; weight?: number; style?: "normal" | "italic"; // for fonts
}
interface DesignStyles {
colors: Record<Id, { name: string; value: Color }>;
text: Record<Id, { name: string; style: TextStyle; paragraph?: Partial<ParagraphStyle> }>;
effects: Record<Id, { name: string; effects: Effect[] }>;
variables: Record<string, string | number | boolean>; // e.g. "brand.primary": "#ffb02e"; referenced as {"$var":"brand.primary"}
}Anywhere a Color is expected, a value may instead reference a variable or a color style:
{ "type": "solid", "color": { "$var": "brand.primary" } }{ "type": "solid", "color": { "$style": "c0l0rst7le01" } }Assets are referenced by id from nodes (ImageNode.assetId, image fills, SVG and video nodes). AssetRef.url is filled in by clients at load time and never persisted; storageKey is the server-side truth.
Invariants#
These hold after every transaction. Operations that would break them are dropped, never applied partially.
nodes["root"]exists,type: "root",parent: null; every other node has exactly one parent and appears exactly once in that parent'schildren. Pages are children of root only; pages cannot nest. Non-container nodes have nochildren.- No cycles. Ids unique.
childrenids all exist. opacityin [0,1];width,height>= 0;rotationfinite; arrays nevernull.- Auto-layout parents own the position/size of children with
sizingfill/hug; engines recomputex,y,width,heightof affected nodes after any transaction (layout is part ofapplyTransactionin the Rust core and in the worker/v1/layout). - Text node
height(autoResize "height" / "width") and group bounds are derived; clients persist the derived values they compute so servers and other clients without a text shaper see consistent boxes.
Example#
A minimal design with one page and a headline:
{
"schema": 1,
"id": "d3k5m7p9r1t3",
"name": "Hello",
"kind": "social-post",
"nodes": {
"root": {
"id": "root", "type": "root", "name": "Root", "parent": null, "children": ["p8k2m4n6q8r0"],
"x": 0, "y": 0, "width": 0, "height": 0, "rotation": 0,
"opacity": 1, "blend": "normal", "visible": true, "locked": false, "effects": []
},
"p8k2m4n6q8r0": {
"id": "p8k2m4n6q8r0", "type": "page", "name": "Page 1", "parent": "root", "children": ["h1a2b3c4d5e6"],
"x": 0, "y": 0, "width": 1080, "height": 1350, "rotation": 0,
"opacity": 1, "blend": "normal", "visible": true, "locked": false, "effects": [],
"clip": true, "unit": "px", "bleed": 0,
"background": [{ "type": "solid", "color": "#0a0a0a" }]
},
"h1a2b3c4d5e6": {
"id": "h1a2b3c4d5e6", "type": "text", "name": "Headline", "role": "headline", "parent": "p8k2m4n6q8r0",
"x": 96, "y": 120, "width": 888, "height": 115, "rotation": 0,
"opacity": 1, "blend": "normal", "visible": true, "locked": false, "effects": [],
"runs": [{ "text": "design at the speed of light." }],
"style": {
"fontFamily": "Instrument Serif", "fontWeight": 400, "fontStyle": "normal", "fontSize": 96,
"fills": [{ "type": "solid", "color": "#ffb02e" }], "letterSpacing": -0.02,
"textCase": "none", "decoration": "none"
},
"paragraph": {
"align": "left", "direction": "ltr", "lineHeight": { "unit": "percent", "value": 100 },
"spacing": 0, "indent": 0, "list": "none"
},
"autoResize": "height", "verticalAlign": "top"
}
},
"assets": {},
"styles": { "colors": {}, "text": {}, "effects": {}, "variables": {} },
"meta": { "createdAt": 1792483200000 }
}