# Document model > The Document → Page → Section hierarchy. One source of truth shared by the canvas, the PDF generator, and the headless hook. Source: https://docs.docmosaic.com/docs/concepts/document-model The document model is a single normalized tree shared between the canvas, the PDF generator, and the headless `useDocumentState` hook. Everything else in the library reads from it; nothing else holds long-lived state. ## The shape ```ts interface Document { id: string; name: string; pageSize: PageSize; // 'A4' | 'LETTER' | ... orientation: PageOrientation; // 'portrait' | 'landscape' pages: Page[]; sections: Section[]; // flat across all pages; each section links to its page via a 1-based `page` number createdAt: Date; updatedAt: Date; } ``` Two design choices to call out: - **Sections are flat**, not nested under pages. Each `Section` carries a 1-based `page` field. This keeps reordering and cross-page moves cheap (no array surgery to lift a section out of one tree into another) and makes the reducer's update logic a single map over one array. - **Document-level `pageSize` and `orientation`**. All pages in a document share the same paper size - the editor is built for documents, not arbitrary multi-format canvases. If you need per-page sizes, fork the reducer; it's pure. ## Page ```ts interface Page { id: string; backgroundPDF: string | null; background?: { color?: string; image?: string }; guides?: PageGuides; } ``` `Page` is intentionally thin - it's a slot, not a container. The expensive content (sections) is keyed off the section's `page` number, so a page can be moved or deleted without rewriting every section underneath it. `background` and `guides` are both optional. Documents migrated from earlier versions render identically - the renderer treats `undefined` as "no override." ## Section ```ts interface Section { id: string; page: number; // 1-based page number type: 'image' | 'text' | 'shape' | 'drawing' | 'frame'; x: number; // PDF points (72 DPI) y: number; width: number; height: number; zIndex: number; // higher renders on top; see Layers hidden?: boolean; // skip on canvas + PDF locked?: boolean; // refuse selection/drag/resize parentFrameId?: string; // set when dropped inside a container frame // ...type-specific payload fields } ``` The type-specific payload differs: - **`image`** - `imageUrl`, an optional `crop` rectangle, and optional `maskShape: 'rect' | 'circle' | 'line'` (a placeholder/image-mask frame). - **`text`** - `text`, `fontSize`, and optional `fontFamily`, `fontWeight`, `fontStyle`, `color`, `align`, `lineHeight`. - **`shape`** - `shape: 'rect' | 'circle' | 'line'`, `fill`, `stroke`, `strokeWidth`, `opacity`. - **`drawing`** - an array of stroke paths in points. - **`frame`** - a container box: optional `fill`, `stroke`, `strokeWidth`, `radius`. Other sections become its children. The reducer doesn't care about the payload - it routes updates through `updateSection(section)` (you pass the whole updated `Section`). Per-type behavior lives in the canvas primitives and the PDF generator. ## Container frames A `FrameSection` (`type: 'frame'`) is a box that **owns** other sections. Children stay in the same flat `sections` array - they're linked to their frame only by a `parentFrameId` back-pointer, never nested. This keeps the tree flat (the design choice above) while still letting a frame move, duplicate, or delete as a unit: - Dragging a section so its center lands inside a frame stamps `parentFrameId`; dragging it out clears it. - Moving a frame translates its children with it; deleting or duplicating a frame cascades to them. - At render time a frame draws **behind** its children (see [Layers](/docs/concepts/layers)). See the [Frames concept](/docs/concepts/frames) for container vs. placeholder (image-mask) frames. ## Where geometry lives Every `x/y/width/height` is in **PDF points**. Always. The canvas applies `finalScale = pageScale * zoom` at display time; the PDF generator hands the values straight to `jspdf` without any conversion. See [Unit system](/docs/concepts/unit-system) for the conversion API and why this matters. ## Mutating the document There is exactly one way to mutate the document: dispatch through the reducer. Two surfaces wrap it: ```ts // 1. The headless hook - owns history internally. const { document, actions } = useDocumentState(); actions.addSection({ type: 'image' }); // appends to the current page // 2. Editor.Root - same hook, mounted under context. {/* primitives call useEditor().actions */} ``` Both paths funnel through `reducer` + `withHistory` from `@docmosaic/core`. If you add a new mutator, route it through there or the timeline will desync. ## Serialization `Document` is plain JSON. Send it over the wire, persist it to localStorage, embed it in a Postgres `jsonb` column - the model is intentionally serialization-friendly. ```ts const json = JSON.stringify(document); const restored: Document = JSON.parse(json); ``` The only thing to watch for: `imageUrl` is stored as a data URL by default. Documents with embedded images can get large. See the [persisting templates recipe](/docs/recipes/persisting-templates) for a pattern that keeps blobs on the side. ## See also - [Designer](/docs/concepts/designer) - the mental model behind the UI - [History](/docs/concepts/history) - undo/redo and the timeline - [Unit system](/docs/concepts/unit-system) - why geometry is stored in points