document structure

The top-level CRDT document structure, initialization defaults, ID system, and metadata.

Top-Level Shared Types

An Ideallo document is a Y.Doc with 6 named shared types:

Map Key Yjs Type Description
o Y.Map<StoredObject> All canvas objects keyed by ObjectId
r Y.Array<string> Z-order array of ObjectId values (index 0 = backmost)
m Y.Map Document metadata
txt Y.Map<Y.Text> Text content keyed by ObjectId (Quill Delta format)
geo Y.Map<Y.Array<number>> Polygon vertex arrays keyed by ObjectId (flat [x, y, x, y, ...])
paths Y.Map<string> SVG path strings keyed by ObjectId
Why compact map keys?

Map keys like o, m, txt are used instead of objects, metadata, texts because these keys appear in every Yjs sync message. Shorter keys reduce wire overhead during real-time collaboration without affecting readability – this documentation provides the complete mapping.

Accessing the Document

import * as Y from 'yjs'

const yDoc = new Y.Doc()

// Access each shared type by its map key
const objects  = yDoc.getMap('o')      // StoredObject entries
const zOrder   = yDoc.getArray('r')    // Z-order (ObjectId list)
const meta     = yDoc.getMap('m')      // Metadata
const texts    = yDoc.getMap('txt')    // Y.Text entries
const geometry = yDoc.getMap('geo')    // Y.Array<number> entries
const paths    = yDoc.getMap('paths')  // SVG path strings

ID System

All entity IDs use 72-bit entropy encoded as 12 base64url characters, generated client-side via crypto.getRandomValues().

Ideallo uses a single branded type for compile-time safety:

Branded Type Used In Example
ObjectId o map keys, txt/geo/paths map keys, tid/gid/pid field values "aB3x_Qm7kL9p"

Unlike Prezillo’s 6 branded types, Ideallo only needs ObjectId because there are no containers, views, styles, or templates. The same ID type is used for both object identifiers and content map keys.

Linked copy IDs

Objects with text, geometry, or path content normally use their own ObjectId as the key in the corresponding content map. Linked copies override this with explicit tid, gid, or pid fields pointing to a different object’s content entry. See Linked Copies for details.

Metadata

The m map stores document-level settings as individual key-value entries:

Key Type Default Description
initialized boolean true Set during initialization, prevents re-initialization
name string "Untitled Board" Document name
backgroundColor string "#f8f9fa" Canvas background color
gridSize number Grid spacing in pixels (if grid enabled)
snapToGrid boolean Whether objects snap to grid

Document Initialization

When a new empty document is created, the following defaults are established in a single Yjs transaction:

yDoc.transact(() => {
    doc.m.set('initialized', true)
    doc.m.set('name', 'Untitled Board')
    doc.m.set('backgroundColor', '#f8f9fa')
})

The initialized flag prevents re-initialization when the document is reopened. Unlike Prezillo, there are no default layers, views, styles, or palette entries – just the metadata.

Z-Order

The r shared type is a Y.Array<string> that defines the front-to-back rendering order of all objects on the canvas. Each entry is an ObjectId. Index 0 is the backmost object; the last index is the frontmost.

Why a Separate Array?

Z-order is stored as a standalone Y.Array rather than as a numeric property on each object because:

  • Reordering is a list operation: Moving an object forward or backward changes the position of one entry relative to others. A CRDT array handles concurrent reorder operations (insert/delete at positions) naturally, while numeric z-index values on objects would require renumbering and create conflicts.
  • Rendering order is global: The canvas needs a single authoritative ordering of all objects. A separate array makes this explicit and avoids scanning every object to reconstruct the order.

Operations

Operation Implementation
Add object r.push([objectId]) – new objects appear on top
Delete object Find index of objectId in r, then r.delete(index, 1)
Bring to front Delete from current position, r.push([objectId])
Send to back Delete from current position, r.insert(0, [objectId])
Move forward/backward Delete from current position, r.insert(newIndex, [objectId])

Separate Content Maps

Ideallo uses three separate content maps to store data that cannot be embedded directly in object entries:

Text Content (txt)

Stores Y.Text instances for Text (T) and Sticky (S) objects. Uses Quill Delta format for rich text operations (bold, italic, links, etc.). The map key is the object’s own ObjectId (or the tid value for linked copies).

Polygon Geometry (geo)

Stores Y.Array<number> instances for Polygon (P) objects. Vertices are stored as flat interleaved pairs: [x1, y1, x2, y2, ...]. Using Y.Array allows efficient incremental updates – new points can be appended without replacing the entire vertex set.

Freehand Paths (paths)

Stores SVG path strings (e.g., "M 0 0 C 10 5 20 10 30 15") for Freehand (F) objects. Unlike geometry, paths are stored as plain strings rather than Y.Array because freehand strokes are committed as a complete unit after the stroke ends (not incrementally during drawing).

Why separate maps?

Y.Text and Y.Array are Yjs shared types that need their own identity for collaborative editing – they cannot be stored as plain JSON values inside a Y.Map entry. Even paths uses a separate map for consistency and to support the linked copy pattern where multiple objects share the same content entry.