# Documents and kits

A document is Grafloria's versioned, shared representation of a diagram; a kit turns domain data into specs plus the wiring that makes those specs interactive.

The document is the API: save the live model, not a framework's projection of it. Framework bindings are thin skins over that model, so the persistence boundary does not depend on which binding draws the canvas.

## How the parts fit together

```mermaid
flowchart LR
  Data["Domain data"] --> Kit["Kit builder"]
  Kit --> Spec["Specs + finalize"]
  Spec --> Mount["Mounted instance"]
  Mount --> Save["Serialize live model"]
  Save --> Document["Shared document"]
  Document --> Load["fromDocument()"]
  Load --> Spec
```

[`render`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#render) mounts a spec and returns a [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance). It invokes the spec's `finalize` after creating the instance. That ordering matters: a kit can declare nodes and edges before mounting, but row interactions, positioned labels and dashboard grid bindings need the live instance.

[`erDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#erdiagram) builds table cards with column-level ports. [`umlDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#umldiagram) builds class cards; its finalization adds multiplicity labels. [`dashboard`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-functions#dashboard) builds widgets and wires their boards. Pass the whole returned spec to the host rather than extracting only its `nodes` and `edges`.

## Save data, restore runtime behavior

[`DiagramSerializer`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-serialization#diagramserializer) provides two persistence shapes:

| Shape | What you get | Use |
| --- | --- | --- |
| `serialize(model)` | A flat object with serializer format `version` and the model's `diagramVersion` | Existing flat-format storage |
| `serializeEnvelope(model)` | A portable envelope containing the document, writer identity, creation time and an integrity checksum | New persistence |

The inner document has a `schemaVersion` separate from the model's mutation counter. Loading runs schema migrations; an envelope checksum mismatch throws instead of silently loading altered data.

The [`DiagramModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-diagrammodel#diagrammodel) serializes nodes, links, groups, metadata and viewport, plus ink and comments when present. [`fromDocument`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#fromdocument) accepts either persistence shape or its JSON string. It returns a [`LoadedDiagramSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#loadeddiagramspec) containing live models, not rebuilt spec projections. Its finalization restores groups and ink and reattaches ER/UML interaction wiring and dashboard binders. Its widget painter uses the same shipped renderer as the dashboard authoring path.

Functions do not travel in JSON. Supply your application widget painter again through `fromDocument`'s `renderWidget` option. A loaded dashboard also lacks the runtime `responsive` configuration; it starts with the saved column count. See [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) for the distinction between documents and framework spec projections.

### A mounted document round trip

Run this in a browser project. The first canvas shows Customer and Order table cards joined at their `id` and `customer_id` rows; the second opens the saved document with the same cards and field-port connection.

```bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer
```

Use this small kit builder in the examples below. Its return value is typed by the library's builder rather than a handwritten copy of its spec type.

```ts title="schema.ts"
import { erDiagram } from '@grafloria/element';

export function buildSchema() {
  return erDiagram({
    entities: [
      { id: 'CUSTOMER', name: 'Customer', position: { x: 40, y: 60 }, columns: [
        { name: 'id', type: 'int', pk: true },
        { name: 'email', type: 'varchar' },
      ] },
      { id: 'ORDER', name: 'Order', position: { x: 360, y: 60 }, columns: [
        { name: 'id', type: 'int', pk: true },
        { name: 'customer_id', type: 'int', fk: true },
      ] },
    ],
    relationships: [{ from: 'ORDER.customer_id', to: 'CUSTOMER.id' }],
  });
}
```

> **Known issue:** `render(fromDocument(json), host)` installs the loaded entities into a new model but does not adopt the saved diagram identity, diagram-level metadata, comments or viewport. Until it is fixed, attach the loaded model to an engine and pass its camera values explicitly when mounting.

The workaround still uses `render` for mounting and kit finalization. [`DiagramEngine`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-engine#diagramengine) is needed here only to retain the entire deserialized model, rather than transferring its entities into a fresh one.

> **Known issue:** Panning and zooming the mounted canvas do not update `model.viewport`. Until it is fixed, copy the instance camera into the model immediately before serialization, as `saveDocument()` does below. Call it again whenever you save after a camera change.

```ts title="main.ts"
import { render, fromDocument } from '@grafloria/element';
import { DiagramEngine, DiagramSerializer } from '@grafloria/engine';
import { buildSchema } from './schema';

const originalHost = document.createElement('div');
const reopenedHost = document.createElement('div');
for (const host of [originalHost, reopenedHost]) {
  host.style.height = '400px';
  document.body.appendChild(host);
}

const original = render(buildSchema(), originalHost);
const serializer = new DiagramSerializer();
export function saveDocument() {
  const model = original.getModel();
  model.viewport = {
    ...original.viewport.getViewport(),
    zoom: original.viewport.getZoom(),
  };
  return JSON.stringify(serializer.serializeEnvelope(model));
}
const json = saveDocument();
const loaded = fromDocument(json);
const engine = new DiagramEngine();
engine.setDiagram(loaded.model);
const reopened = render(loaded, reopenedHost, {
  engine,
  viewport: { x: loaded.model.viewport.x, y: loaded.model.viewport.y },
  zoom: loaded.model.viewport.zoom,
});

// Call when your application removes these canvases.
export function unmount() {
  reopened.dispose();
  original.dispose();
  reopenedHost.remove();
  originalHost.remove();
}
```

![Customer and Order table cards with their field-port connection on the original and reopened canvases.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/8f8382a48c2da78208bfc9693ed024b7.png)

This saves document data, not an undo stack or a serialized renderer. The loader reconstructs the kit's runtime wiring after mounting.

## The same kit contract in each binding

Each host below renders the same two table cards from `schema.ts` and runs their finalization through `render`. Start in your framework project; install the binding you use in addition to the shared packages above.

### React

Use [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriadiagram) for the complete kit spec. The component disposes its instance on unmount.

```bash
npm install @grafloria/react react react-dom
```

```tsx title="SchemaView.tsx"
import { GrafloriaDiagram } from '@grafloria/react';
import { buildSchema } from './schema';

const spec = buildSchema();

export default function SchemaView() {
  return <div style={{ height: '400px' }}><GrafloriaDiagram spec={spec} /></div>;
}
```

![React renders Customer and Order cards with PK and FK rows joined by a connection.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/be27f1ec56a544029199bbad11ba90a9.png)

### Vue

Pass the spec to Vue's [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue#grafloriadiagram). Its `ready` event provides the mounted instance when you need to serialize it.

```bash
npm install @grafloria/vue vue
```

```vue title="SchemaView.vue"
<script setup lang="ts">
import { GrafloriaDiagram } from '@grafloria/vue';
import { buildSchema } from './schema';

const spec = buildSchema();
</script>

<template>
  <div style="height:400px"><GrafloriaDiagram :spec="spec" /></div>
</template>
```

Vue renders Customer and Order cards with their column types and field-port connection.

### Angular

[`GrafloriaDiagramComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-classes#grafloriadiagramcomponent) is the generic kit host, not a canvas assembled from projected node arrays. Its `ready` output provides the live instance.

```bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser rxjs
```

```ts title="schema-view.component.ts"
import { Component } from '@angular/core';
import { GrafloriaDiagramComponent } from '@grafloria/angular';
import { buildSchema } from './schema';

@Component({
  selector: 'app-schema-view',
  standalone: true,
  imports: [GrafloriaDiagramComponent],
  template: '<grafloria-diagram [spec]="spec" style="height:400px" />',
})
export class SchemaViewComponent {
  readonly spec = buildSchema();
}
```

Angular renders Customer and Order table cards connected at their rows.

### Qwik

Qwik's [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriadiagram) builds the kit in the browser through `spec$`. Do not put function-bearing kit specs in resumable state. The builder runs once per mount; change the component `key` when new schema data needs a fresh build.

```bash
npm install @grafloria/qwik @builder.io/qwik
```

```tsx title="schema-view.tsx"
import { component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaDiagram } from '@grafloria/qwik';
import type { DiagramInstance } from '@grafloria/renderer';
import { buildSchema } from './schema';

export default component$(() => {
  const schemaVersion = useSignal(1);
  const instance = useSignal<NoSerialize<DiagramInstance>>();

  return (
    <GrafloriaDiagram
      key={schemaVersion.value}
      spec$={() => buildSchema()}
      onReady$={(ready) => { instance.value = noSerialize(ready); }}
      style={{ height: '400px' }}
    />
  );
});
```

Qwik renders Customer and Order table cards with PK and FK badges and a connection.

Create live transports and stores in `useVisibleTask$` and retain them with `noSerialize()`, as with the instance above. A [`CommentStore`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-comments#commentstore) holds subscribers, not resumable data. On [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow), a live collaboration object likewise needs `noSerialize()`; plain node and edge data do not. See [Qwik: custom content and kits](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-custom-content-and-kits) for browser-side kit construction.

## Mermaid is a body plus a sidecar

`instance.exportText()` returns Mermaid-compatible text with a `%%grafloria:document` comment by default. Mermaid ignores that comment; Grafloria reads the document inside it. This preserves data that the Mermaid body cannot express, including exact geometry and styling.

The sidecar deliberately excludes selected, hovered and focused entity state and derived link polylines. User-authored manual bends remain. “Lossless” here means document intent, not every viewer's transient state or every routed pixel.

The following browser sample uses a [`DiagramSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#diagramspec) to mount a labeled connection, exports its text, then loads it into a second canvas. You see Start connected to Finish on both canvases.

```ts title="text-round-trip.ts"
import { render, type DiagramSpec } from '@grafloria/element';

const spec: DiagramSpec = {
  nodes: [
    { id: 'start', label: 'Start', position: { x: 40, y: 80 } },
    { id: 'finish', label: 'Finish', position: { x: 340, y: 80 } },
  ],
  edges: [{ id: 'route', source: 'start', target: 'finish', label: 'next' }],
};
const firstHost = document.createElement('div');
const secondHost = document.createElement('div');
for (const host of [firstHost, secondHost]) {
  host.style.height = '400px';
  document.body.appendChild(host);
}
const first = render(spec, firstHost);
const text = first.exportText();
const second = render(spec, secondHost);
const result = second.loadText(text);
console.log(result.source, result.bodyEdited, result.sidecarInvalid);

export function unmount() {
  second.dispose();
  first.dispose();
  secondHost.remove();
  firstHost.remove();
}
```

![Both canvases show Start connected to Finish by an arrow labeled next.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/4e46898c885b232a73d861d427ead8e5.png)

`loadText()` returns an [`ImportTextResult`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-serialization#importtextresult) and reconciles into the existing model, preserving its listeners and plugins. In the default `prefer: 'auto'` mode, an unchanged body uses the sidecar; an edited body applies structure and labels on top of the sidecar data. Invalid sidecar JSON sets `sidecarInvalid` and falls back to parsing the body. Unsupported or invalid diagram text makes `loadText()` throw before changing the canvas.

`loadText()` is an entity reconciliation path, not a whole-document camera or comment restoration path. Use the document loader above when opening a complete saved document. See [Import diagram text and files](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/import-diagram-text-and-files) for format selection and parse results.

## Dashboard snapshots are a domain format

A dashboard combines geometry, widget state, and persistence/history in one document. A widget's kind, payload and grid cell describe the same live board that gestures edit; you do not maintain a second widget registry beside the layout.

The dashboard handle's `toJSON()` returns dashboard authoring data that `dashboard()` accepts again. It reads live cells and membership, including nested containers, rather than the original widget arrays. Unlike a generic framework spec projection, it is a supported domain round trip. Unlike the shared diagram document, it describes dashboard options, views and widgets rather than arbitrary diagram entities.

This browser sample renders the shipped KPI widget twice: once from authoring data and once from the mounted board's snapshot. No custom widget painter is needed.

```ts title="dashboard-round-trip.ts"
import { dashboard, render } from '@grafloria/element';

const firstHost = document.createElement('div');
const secondHost = document.createElement('div');
for (const host of [firstHost, secondHost]) {
  host.style.height = '400px';
  document.body.appendChild(host);
}
const spec = dashboard({
  columns: 4,
  widgets: [{
    id: 'revenue', kind: 'kpi', span: 2,
    data: { label: 'Revenue', value: '$6.8M', delta: 12.4 },
  }],
});
const first = render(spec, firstHost);
const saved = spec.handle.toJSON();
const restoredSpec = dashboard(saved);
const second = render(restoredSpec, secondHost);

export function unmount() {
  second.dispose();
  first.dispose();
  secondHost.remove();
  firstHost.remove();
}
```

![Original and restored Revenue KPI cards each show $6.8M and a green 12.4% comparison.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/00cb2d812acd12e0111a1c0f66e7672f.png)

The snapshot omits the `renderWidget` and `onLayoutChange` function seams; supply them again when rebuilding a board that uses them. Use `DiagramSerializer` instead when saving the shared diagram document, and `fromDocument` to regain both its live models and its dashboard handle.

## Explore next

- [Live ER diagram](https://grafloria.com/demos/diagrams/table-er.html) — table cards and relationships from domain data.
- [Live UML diagram](https://grafloria.com/demos/diagrams/class-uml.html) — class compartments and relationship markers.
- [Live dashboard builder](https://grafloria.com/demos/dashboard/dashboard-builder.html) — widgets, board geometry and editing together.
- [Build a dashboard](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/build-a-dashboard) and [Arrange dashboard containers](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/arrange-dashboard-containers) — author and edit boards.
- [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) — persistence procedures and projection boundaries.
- [Commands and history](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history) — how user-facing edits join the history stack.
