---
title: Documents
description: Create, load, share and destroy an EdytorDocument, and understand when it is ready to edit.
icon: file-text
---

An `EdytorDocument` is the thing views render and providers sync. Create one yourself when you want to share it between views, edit it without a view, attach a provider from code, or control its lifetime.

## Create a document

```ts
import { createDocument, defaultSemantics } from 'edytor';

const document = createDocument({
  value: { children: [{ type: 'paragraph', id: 'p1', content: [{ text: 'hello' }] }] },
  actor: { id: 'user-42', name: 'Ada', color: '#7559ee' },
  semantics: defaultSemantics
});

document.readiness; // 'local'
document.transact(() => document.facade.insertText('p1', 5, ' world'));
document.facade.blockText('p1'); // 'hello world'
document.history.undo(); // 'hello' again; the seed itself is never an undo step
```

| Option | Type | Description |
| --- | --- | --- |
| `value` | `JSONDoc` | Initial content, seeded immediately: the document is ready at once. Without it, the document stays `pending` until a provider or `sync()` decides. To seed only when a room turns out empty, pass `value` to `attachSync` instead (see the warning below). |
| `actor` | `{ id, name?, color? }` | The local author. `id` is stored in attribution; `name` and `color` are published as presence. Defaults to an anonymous `anon-<uuid>`. |
| `awareness` | `Awareness` | Use an existing awareness instance instead of creating one. The document does not destroy a borrowed instance. |
| `history` | `{ captureTimeout?: number }` | Edits closer together than this many milliseconds merge into one undo step. Engine default: 500. `0` makes every commit its own step. |
| `semantics` | `DocumentSemanticsConfig` | Block roles (void, island, lines), kinds without content, default child types and the default block type. Views contribute these from their plugins; a document you edit without a view knows none of them until you pass them. `defaultSemantics` holds the bundled rich-text, code and image kinds (`richTextSemantics`, `codeSemantics`, `imageSemantics` each hold one plugin's). |
| `lineage` | `{ depth?: number }` | Keep up to `depth` earlier versions of each block, captured when another author, a delete or an undo replaces it. Off by default; read them with `attribution.history(id)`. |

Without `semantics`, a document edited with no view attached applies every edit the tree allows: it merges text into a divider or moves a code line out of its code block, which no view can render. Pass `defaultSemantics`, or your plugins' rules, whenever you edit headlessly. It is not the default because a view whose plugins redefine one of those kinds would then throw `SemanticConflictError`.

:::warning
`createDocument({ value })` seeds before any provider is attached. Do not follow it with `attachSync` to a room that may already hold blocks with the same ids: the two copies meet as described in [Deterministic seeds](#deterministic-seeds), and an earlier seed's block edited since can be replaced. Create the document without `value` and pass `value` to `attachSync`, which seeds only when the room turns out empty.
:::

## Load saved bytes

`document.encode()` returns the full replicated state as a `Uint8Array`. `loadDocument` restores it as a fresh replica with a new client id:

```ts
import { loadDocument } from 'edytor';

const bytes = document.encode(); // store or send it
const copy = loadDocument(bytes); // readiness 'hydrated'
copy.facade.toJSON(); // same JSON as the source
```

`loadDocument` checks the bytes on a scratch copy first. Corrupt bytes throw `UndecodableUpdateError`, a schema this build cannot own throws `SchemaMismatchError`, and a v13 (`yjs`) document throws `UnsupportedDocError` with reason `legacy`; see [Migration](/docs/reference/migration). Your bytes are never modified.

## The document object

| Member | Description |
| --- | --- |
| `facade` | The document API: reads, operations, order queries and `onChange`. See the [document API reference](/docs/reference/document-api). |
| `history` | The shared undo manager (`undo()`, `redo()`, `stopCapturing()`). Throws `DocumentNotReadyError` while the document is `pending`. |
| `clearHistory()` | Empty the undo and redo stacks. Stacks are otherwise unbounded. |
| `awareness` | The one presence instance for every view and provider. See [Presence](/docs/collaboration/presence). |
| `actor` | The local author identity. |
| `attribution` | Per-block authorship: `block(id)` returns `createdBy`, `contributors` and `lastChangedBy`. |
| `transact(fn)` | Run several edits as one transaction and one undo step. A throw from `fn` does not undo the writes made before it: they are kept and synced. |
| `encode()` | The full state as bytes, for `loadDocument`. |
| `attachSync(sync, { value? })` | Attach a provider to the document. Returns its cleanup. |
| `ready`, `readiness`, `onReady(fn)` | Whether the content has been decided, and how. See below. |
| `syncRefusal`, `onSyncRefused(fn)` | The `SyncRefusedError` a provider reported when the server refused this client (close `1008`, or `4xxx` other than `4401`). It lasts until that provider is released or syncs again. |
| `sync(value?)` | Decide a `pending` document now: seed `value` if it is empty, adopt its content otherwise. |
| `writable`, `onWritableChange(fn)` | `false` while the document holds content from a schema this build cannot own. Every write is refused and providers stop persisting and sending until it heals. |
| `clientID` | This replica's CRDT client id (`doc.clientID`). |
| `doc` | The underlying CRDT document, for providers and advanced code. |
| `destroy()` | Release the document: providers, history, awareness and the CRDT document. |

## Readiness

A document created without a `value` might be about to receive content from a provider, so it does not seed anything until that question is settled.

| `readiness` | Meaning | What works |
| --- | --- | --- |
| `pending` | Undecided: a provider may still bring content. | Reads, `attachSync`, `sync()`, `destroy()`. `history` throws. |
| `local` | This replica seeded the content. | Everything. |
| `hydrated` | The content came from a provider or from `loadDocument`. | Everything. |

With providers attached, the rule is *settle or bound*:

- A document that already holds content becomes `hydrated` as soon as any provider settles, or is refused.
- An empty document seeds its `value` only once every attached provider has settled, failed, been removed, or reached its bound.
- IndexedDB always settles, so it has no bound. A WebSocket alone in a new room may never hear from anyone, so it gets `DEFAULT_READINESS_BOUND` (1000 ms), counted from the moment the socket opens or first fails to open: a room that is slow to accept the connection is waited for, up to the socket's `connectTimeout` (10 s by default). Expired credentials (`4401`) before the first sync hold the bound: an empty document waits for a dial that gets in.
- A server refusal (`SyncRefusedError`) never seeds: an empty document stays `pending` and reports it in `syncRefusal` and `onSyncRefused`. A document that already has content keeps it and becomes `hydrated`, so a view mounted on it shows that content. The refusal belongs to the provider that reported it: once that provider is released (its cleanup ran) or syncs again (`connect()` after a refreshed token), the rule above applies to the providers still attached, and an empty room seeds `value`. Released with no provider left, the document stays `pending` until the next `attachSync` or `sync()`.

```ts
if (!document.ready) {
  const off = document.onReady(() => console.log(document.readiness));
}
```

`onReady` fires once, at the transition; it is never called for a document that is already ready.

## Attach a provider

`attachSync` takes the same factories as the `<Edytor sync>` prop. The document keeps one provider per target (one IndexedDB name, one server and room), so attaching the same target twice does nothing.

```ts
import { createDocument, createWebsocketSync } from 'edytor';

const document = createDocument({ actor: { id: 'user-42' } }); // pending
document.attachSync(
  createWebsocketSync({ server: 'wss://rooms.example.com/rooms', room: 'doc-42' }),
  { value: { children: [] } } // seeded only if the room turns out empty
);
```

`document.destroy()` runs every attached provider's cleanup. Calling the returned cleanup yourself detaches that one provider.

## One document, several views

Pass the same document to several `<Edytor>` views. They share one document API, one undo history and one awareness; undo restores the caret in the view that issued it.

```svelte
<script lang="ts">
  import { onDestroy } from 'svelte';
  import { Edytor, createDocument, createIndexeddbSync } from 'edytor';

  const document = createDocument();
  document.attachSync(createIndexeddbSync('notes/today'));
  onDestroy(() => document.destroy());
</script>

<Edytor {document} />
<Edytor {document} readonly />
```

A view never destroys a document you passed in: you created it, you destroy it. Views contribute the structural rules of their plugins (void and island blocks, default children) to the document. Two views whose plugins disagree on a rule throw `SemanticConflictError`. See [the Edytor component](/docs/editor/edytor-component) for the view's props.

## Headless use (Node and SSR)

Import from `edytor/crdt/edytor` in code that cannot load Svelte components. It exports the same document API as `edytor`, without the component:

```ts title="src/lib/server/append-line.ts" check
import { defaultSemantics, loadDocument, toBlockSpec } from 'edytor/crdt/edytor';

export function appendLine(saved: Uint8Array, text: string): Uint8Array {
  const document = loadDocument(saved, { actor: { id: 'server-bot' }, semantics: defaultSemantics });
  const facade = document.facade;
  const index = facade.childrenIds(null).length; // append at the root
  facade.insertBlock({ parent: null, index }, toBlockSpec({ type: 'paragraph', content: [{ text }] }));
  const bytes = document.encode();
  document.destroy();
  return bytes;
}
```

:::warning
Do not create an `EdytorDocument` on the document a sync server holds. See [writing your own server](/docs/server/protocol#never-attach-a-document-on-the-server).
:::

## Deterministic seeds

Seeding a `value` is deterministic: the seed is written under a writer id hashed from the value, and blocks without an `id` get ids derived from the same hash. Two replicas that seed the same template write the same data, so their seeds merge into one copy instead of two, and a late identical seed never erases an edit.

```ts
const template = { children: [{ type: 'paragraph', content: [{ text: 'Agenda' }] }] };
const a = createDocument({ value: template });
const b = createDocument({ value: template });
// Merging a and b shows one "Agenda" paragraph, not two.
```

Seeds are not undo steps and carry no author (`createdBy` is absent on seeded blocks). Seeds of different values merge as a union: blocks with different ids all show, so give a template new block ids when its content changes.

When a seed shares a block id with content it meets, one version of that block wins, last-writer-wins by writer id:

| The block was written by | Result |
| --- | --- |
| A person editing (a live replica) | The seed loses. Seed writer ids sit below 2^26 and live ones are random 53-bit ids, so the block and every edit in it stay. |
| The same seed | Nothing changes: it is the same data. |
| A different seed | The larger writer id wins, whichever arrived first. If the loser is the room's copy, its edits since that seed are replaced, on every replica, and no undo brings them back. |

So never pass a snapshot that changes (a copy of `onChange` output) as `value` beside a room: a client that seeds it late, because it could not reach the room before its readiness bound, puts that snapshot in the last row. Pass a fixed template, or no `value` at all.

:::note
Documents seeded by an older build used a full 32-bit writer id. A template without block ids that a new build seeds late into such a document gets new derived ids, so it shows twice, once. Templates with ids are unaffected.
:::

## Errors

| Error | When |
| --- | --- |
| `DocumentNotReadyError` | `history` used while the document is `pending`. |
| `DocumentDestroyedError` | The document is used after `destroy()`, or `attachSync` runs on a destroyed document. |
| `SemanticConflictError` | A view or option declares a block rule that contradicts one the document already adopted. |
| `UndecodableUpdateError` | `loadDocument` bytes cannot be decoded. |
| `UnsupportedDocError` | A foreign object, or a v13 document (reason `legacy`). |
| `SchemaMismatchError` | The content claims a schema this build cannot own. |
