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 parent and ordered children. Any node can be read or patched directly.
  • JSON, camelCase. The TypeScript definitions are the source of truth; @desyn/schema derives zod and JSON Schema from them and desyn-core mirrors 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#

TypeScript
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.

docs/DOCUMENT_MODEL.md
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:

JSON
{ "type": "solid", "color": { "$var": "brand.primary" } }
JSON
{ "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.

  1. nodes["root"] exists, type: "root", parent: null; every other node has exactly one parent and appears exactly once in that parent's children. Pages are children of root only; pages cannot nest. Non-container nodes have no children.
  2. No cycles. Ids unique. children ids all exist.
  3. opacity in [0,1]; width,height >= 0; rotation finite; arrays never null.
  4. Auto-layout parents own the position/size of children with sizing fill/hug; engines recompute x,y,width,height of affected nodes after any transaction (layout is part of applyTransaction in the Rust core and in the worker /v1/layout).
  5. 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:

design.json
{
  "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 }
}

Continue#

Popular pages

QuickstartGet started
Keyboard shortcutsEditor
The agentAI
Docker Compose quickstartSelf-hosting
Configuration referenceSelf-hosting
Agent API (v1)Developers