# Migrate from v1

> Move a browser editor from SuperDoc v1 to the v2 DOCX engine.




If your v1 application mounts `SuperDoc` from the package root, waits for readiness, and exports through the same instance, most of that integration remains valid. The migration is mainly about selecting the v2 package and removing dependencies on v1 internals.

> **This guide covers local browser editing (note)**
>
> V2 collaboration rooms use a different document format and require a separate migration. Do not connect a v2 editor
> directly to an existing v1 collaboration room.


**Migrating with an AI coding agent?** Use this prompt:

```text
Help me migrate this project from SuperDoc v1 to v2.

First, read these sources of truth:
/md/editor/migrate-from-v1/overview.md
/migration/v1-to-v2.json

Inspect the project and report:
1. Removed imports and package subpaths
2. Any direct editor.* access, including commands, state, view, chain(), helpers, comments, presentationEditor, and on()
3. Legacy configuration and collaboration usage
4. Custom UI, extensions, and DOM selectors that require redesign
5. Synchronous Document API reads such as doc.extract(), doc.getMarkdown(), and doc.selection.current(), which the browser resolves as Promises

Do not change code yet. Classify each finding using the migration catalog,
then propose the smallest safe migration sequence and a verification plan.
```


## 1. Install the v2 package [#1-install-the-v2-package]

SuperDoc v2 is selected by the installed package version:

```bash
pnpm add superdoc@2
```

Keep the package name `superdoc`. Do not add `editorVersion`, `v2`, or `v2Integration` to the editor configuration. The v2 package always runs the v2 DOCX engine and has no runtime fallback to v1.

## 2. Keep the supported root integration [#2-keep-the-supported-root-integration]

These browser integration points remain the same:

| Integration point | V2 path                                  |
| ----------------- | ---------------------------------------- |
| Editor class      | `SuperDoc` from `superdoc`               |
| Styles            | `superdoc/style.css`                     |
| Document input    | `document` with a `File`, `Blob`, or URL |
| Ready lifecycle   | `onReady` or the `ready` event           |
| Mode changes      | `setDocumentMode()`                      |
| DOCX output       | `export()`                               |
| Cleanup           | `destroy()`                              |

A minimal v2 mount still looks familiar:

```ts
import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';

const superdoc = new SuperDoc({
  selector: '#editor',
  document: file,
  onReady: ({ superdoc }) => {
    superdoc.setDocumentMode('editing');
  },
});
```

Call document-dependent methods only after `onReady`. V2 opens and renders the DOCX progressively, so readiness is the public boundary for starting product interaction.

## 3. Remove legacy package subpaths [#3-remove-legacy-package-subpaths]

SuperDoc v2 exposes a smaller public package surface:

| V1 import                                                               | V2 replacement                                                                       |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `superdoc/types`                                                        | Not a rename. See [Removed in v2](/editor/migrate-from-v1/removed-apis)              |
| `superdoc/headless-toolbar`                                             | `superdoc.ui`, the controller the instance owns                                      |
| `superdoc/headless-toolbar/react`                                       | Hooks and providers from `superdoc/ui/react`                                         |
| `superdoc/headless-toolbar/vue`                                         | Build on `superdoc.ui`, the framework-agnostic controller                            |
| `superdoc/super-editor`                                                 | Use the `SuperDoc` instance and its public active-editor facade                      |
| `superdoc/converter`, `superdoc/docx-zipper`, or `superdoc/file-zipper` | Use the supported `SuperDoc` load and export workflow; there is no direct v2 subpath |

Do not replace a removed public path with an internal package. Packages such as `@superdoc/v2-host`, `@superdoc/headless`, and `@superdoc/document-api-v2-adapter` are implementation details, not customer integration surfaces.

## 4. Replace direct editor internals [#4-replace-direct-editor-internals]

V1 code could reach through the editor to ProseMirror state and commands:

```ts
new SuperDoc({
  selector: '#editor',
  document: file,
  onEditorCreate: ({ editor }) => {
    const text = editor.state.doc.textContent;
    console.log(text);
  },
});
```

In v2, read and change document content through the public Document API after the editor is ready:

```ts
new SuperDoc({
  selector: '#editor',
  document: file,
  onReady: async ({ superdoc }) => {
    const doc = superdoc.activeEditor?.doc;
    if (!doc) throw new Error('The active document is unavailable.');

    const text = await doc.getText({});
    console.log(text);
  },
});
```

Do not use `editor.state`, `editor.view`, DOM text offsets, or private command objects as the document model. Use the Document API for fresh reads and mutations, and `superdoc/ui` for reactive custom UI state.

### Move UI interactions off the document runtime [#move-ui-interactions-off-the-document-runtime]

`DocumentRendererRuntime` is part of SuperDoc's rendering path. V1 integrations sometimes retrieved it with
`getDocumentRuntimeForDocument()` and called navigation methods on it. That lookup is not the V2 readiness boundary. It
can return `null` when the renderer is not available, which often surfaces as an application error such as
`DocumentRuntime is not available`.

Read the instance-owned controller from `superdoc.ui` after `onReady` instead. The controller groups state and actions by
the feature your interface is showing. It also owns the navigation needed to reveal content on a virtualized page.

#### Scroll to a tracked change [#scroll-to-a-tracked-change]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function scrollToChange(superdoc: SuperDoc, changeId: string): Promise<boolean> {
  const result = await superdoc.ui.trackChanges.scrollTo(changeId);
  return result.success;
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export async function scrollToChange(
  superdoc: SuperDoc,
  documentId: string,
  changeId: string,
): Promise<boolean> {
  const runtime = superdoc.getDocumentRuntimeForDocument(documentId);
  if (!runtime?.scrollToElement) throw new Error('DocumentRuntime is not available.');
  return Boolean(await runtime.scrollToElement(changeId));
}
```


Use the narrowest navigation method for the target you already know:

| Target you have                     | V2 navigation method                                 |
| ----------------------------------- | ---------------------------------------------------- |
| Tracked-change ID                   | `superdoc.ui.trackChanges.scrollTo(changeId)`        |
| Comment ID                          | `superdoc.ui.comments.scrollTo(commentId)`           |
| Content-control ID                  | `superdoc.ui.contentControls.scrollIntoView({ id })` |
| Stable block, comment, or change ID | `superdoc.scrollToElement(elementId)`                |

The feature-specific methods return structured results and keep feature state, such as the active tracked change, in
the same controller. Use `superdoc.scrollToElement()` when you only have a stable extracted ID and do not know its entity
type. It returns `false` when SuperDoc cannot route the navigation.

You do not need to import the normal controller from `superdoc/ui`. The `superdoc.ui` getter creates it once, and
`superdoc.destroy()` owns its cleanup. Import `createSuperDocUI` from `superdoc/ui` only when you need a separately owned
controller with its own lifecycle. Continue with [Custom UI controller setup](/editor/custom-ui/controller-setup) for
subscriptions and cleanup, or the [tracked-changes UI guide](/editor/custom-ui/tracked-changes) for review navigation.

## 5. Map common editor and ProseMirror internals [#5-map-common-editor-and-prosemirror-internals]

These are the mappings most applications need: reading text, resolving a selection, inserting at it, and exporting. Each example includes its imports, inputs, async boundary, and returned value. V2 is selected by default;
switching an example switches every example on this page.

For everything else, work from [Removed in v2](/editor/migrate-from-v1/removed-apis). Each entry names the v1
symbol, what you will observe when it breaks, and the v2 replacement if one exists. Entries link to a canonical API
page where one covers the replacement.

### Document API [#document-api]

#### getText [#gettext]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function readText(superdoc: SuperDoc): Promise<string> {
  const doc = superdoc.activeEditor?.doc;
  if (!doc) throw new Error('The active document is unavailable.');
  return doc.getText({});
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export function readText(superdoc: SuperDoc): string {
  return superdoc.activeEditor!.state.doc.textContent;
}
```


#### selection.current [#selectioncurrent]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function currentSelection(superdoc: SuperDoc) {
  const doc = superdoc.activeEditor?.doc;
  if (!doc) throw new Error('The active document is unavailable.');
  return doc.selection.current({ includeText: true });
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export function currentSelection(superdoc: SuperDoc) {
  return superdoc.activeEditor!.state.selection;
}
```


#### selection.current().selectionTarget [#selectioncurrentselectiontarget]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function selectionTarget(superdoc: SuperDoc) {
  const doc = superdoc.activeEditor?.doc;
  if (!doc) throw new Error('The active document is unavailable.');
  const selection = await doc.selection.current({});
  return selection.selectionTarget;
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export function selectionRange(superdoc: SuperDoc) {
  const { from, to } = superdoc.activeEditor!.state.selection;
  return { from, to };
}
```


#### insert [#insert]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function insertText(superdoc: SuperDoc, text: string) {
  const doc = superdoc.activeEditor?.doc;
  if (!doc) throw new Error('The active document is unavailable.');
  // A targetless insert appends at the end of the document. V1 inserted at
  // the caret, so a missing target is a lost selection, not a default.
  // Failing loudly beats silently putting the text somewhere else.
  const selection = await doc.selection.current({});
  if (!selection.selectionTarget) {
    throw new Error('No selection target. Capture one while the editor has focus, or pass an explicit target.');
  }
  const result = (await doc.insert({ value: text, target: selection.selectionTarget })) as
    | { success: boolean }
    | { receipt: { success: boolean } };
  const receipt = 'receipt' in result ? result.receipt : result;
  if (!receipt.success) throw new Error('Text insertion failed.');
  return result;
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export function insertText(superdoc: SuperDoc, text: string) {
  const editor = superdoc.activeEditor!;
  return editor.view.dispatch(editor.state.tr.insertText(text));
}
```


Pass a target unless you mean to append. `doc.insert({ value })` with no `target` or `ref` routes to the end of the
document, so a direct port of `tr.insertText(text)` appends instead of inserting at the caret. In a browser this
example throws rather than falling back, because losing the selection means the text would land somewhere the user did
not choose. Headless callers with no selection can omit the target deliberately and take the append.

The Editor returns a mutation receipt directly, while the headless SDK returns an operation envelope containing the
receipt. Normalize the two shapes before checking `success`, as shown above. Headless callers can also use the envelope's
`target`, `resolvedRange`, and `context` for details about the applied mutation. Verify persisted output by saving and
reopening the DOCX when insertion is part of an automated workflow.

### Export [#export]

#### getMarkdown [#getmarkdown]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function markdown(superdoc: SuperDoc): Promise<string> {
  const doc = superdoc.activeEditor?.doc;
  if (!doc) throw new Error('The active document is unavailable.');
  return doc.getMarkdown({});
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export function markdown(superdoc: SuperDoc): string {
  return superdoc.activeEditor!.getMarkdown();
}
```


#### export [#export-1]

**V2**

```ts
import type { SuperDoc } from 'superdoc';

export async function exportDocx(superdoc: SuperDoc) {
  return superdoc.export({ exportType: ['docx'], triggerDownload: false });
}
```

**V1**

```ts
import type { SuperDoc } from 'superdoc';

export async function exportDocx(superdoc: SuperDoc) {
  return superdoc.export({ exportType: ['docx'], triggerDownload: false });
}
```


## 6. Migrate changed integration contracts [#6-migrate-changed-integration-contracts]

The following changes are public V2 contracts rather than replacements for ProseMirror internals. Treat the browser
Document API as asynchronous, keep document and UI namespaces explicit, and preserve every returned cleanup function.

### Collaboration [#collaboration]

#### document.v2Collaboration [#documentv2collaboration]

**V2**

```ts
import type { Document, V2CollaborationConfig } from 'superdoc';

export function collaborativeDocument(
  data: Blob,
  id: string,
  serverUrl: string,
  token: string,
  roomMode: 'join' | 'create' = 'join',
): Document {
  const v2Collaboration: V2CollaborationConfig = {
    providerType: 'hocuspocus', documentId: id, serverUrl, token, roomMode,
  };
  return {
    id, type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', data, v2Collaboration,
  };
}
```

**V1**

```ts
import * as Y from 'yjs';
import { HocuspocusProvider } from '@hocuspocus/provider';

const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({ url: serverUrl, name: id, document: ydoc });
const modules = { collaboration: { ydoc, provider } };
```


V2 owns the provider and `Y.Doc`; do not pass an external pair through `modules.collaboration`. Use `roomMode: 'join'`
for an existing room and `'create'` only while seeding a missing room. Wait for `onCollaborationReady` before reading
`instance.provider`, handle `collaboration-v2-room-missing` and `collaboration-v2-room-already-exists` in `onException`,
and let `destroy()` release the owned collaboration lifecycle. V2 has no `join-or-create` mode: retrying with a different
room mode requires a fresh mount.

## 7. Verify one DOCX round trip [#7-verify-one-docx-round-trip]

Before migrating advanced UI or automation, verify the smallest complete path:

1. Open a representative DOCX in v2.
2. Wait for `onReady`.
3. Make one direct or tracked edit.
4. Export the document with `SuperDoc.export()`.
5. Reopen the result in Microsoft Word or another DOCX reader.
6. Confirm the edited content, formatting, comments, and tracked changes still match the intended document state.

Keep v1 and v2 on separate branches or deployments while comparing the same input documents. A package upgrade should remain easy to reverse until the documents that matter to your product complete this round trip.

Once that round trip holds, migrate whatever else your application uses, working from [Removed in
v2](/editor/migrate-from-v1/removed-apis) and the canonical API pages it links to.

Next, follow the [Editor quickstart](/editor/quickstart) for a complete v2 mount, or learn the [Document API mental model](/document-api/mental-model) before migrating programmatic edits.
