Guides

* [Getting started](/developers)
* [Projects and storage](/developers/projects-and-storage)
* [Media, fonts and models](/developers/media)
* [Save and load](/developers/save-and-load)
* [Theme, features and events](/developers/theming-and-events)
* [Driving the engine](/developers/engine)
* [Limits and support](/developers/limits-and-support)
* [Troubleshooting](/developers/troubleshooting)

More

* [Reference](/developers/reference/)
* [Everything as one text file](/developers/llms-full.txt)

# Save and load

A document is the project as plain JSON (JavaScript Object Notation): `version`, `project`, `mediaAssets`, `tracks`, `clips`, `effects`, `keyframes` and `transitions`. The media files are not in it. `saveDocument` returns an array next to the document; this page calls it "the media list". One entry of that list is "a media handle".

## Save

`saveDocument` returns the document and the media list, one media handle per stored file.

```ts
import type { EditorBackend } from "@vidstudio/engine";

export async function inspect(backend: EditorBackend): Promise<void> {
  const { document, media } = await backend.saveDocument();
  console.log(document.project.name, document.clips.length, "clips");
  for (const item of media) console.log(item.assetId, item.name, item.type, item.bytes, item.hash);
}

```

| Field   | Meaning                                                                           |
| ------- | --------------------------------------------------------------------------------- |
| assetId | The asset's id inside the document                                                |
| name    | The file name the user imported                                                   |
| type    | "video", "audio" or "image"                                                       |
| bytes   | The file size                                                                     |
| hash    | "<bytes>:<hex>": the size plus a SHA-256 of the first and last 64 KB              |
| read()  | A File handle to the stored file; no bytes are read until you upload or stream it |

`hash` is cheap on purpose. It is enough to skip an upload your server already has. It is not a checksum of the whole file. Two files of the same size that differ only in the middle share a hash. Compute your own checksum on the server when that matters. An asset marked missing (see below) has no media handle.

## Load

`loadDocument(document, resolve)` replaces the project's content with the document. It calls `resolve(asset)` once per asset in `document.mediaAssets`. Return a `File`, or a web address the engine downloads. Either one is written to file storage as a stream, and proxies are made again in the background. The document's name and settings replace the project's. Every id is minted again. Undo and redo are cleared.

A web address on another origin needs the headers in [Media, fonts and models](/developers/media).

## Missing media

When `resolve` throws, or the download fails, the asset loads with `missing: true`. Its clips show a flat grey frame. `exportProject` rejects with the code `missing-media` while a clip still uses it. The editor opens the project and shows a warning with the count. To repair it, load the document again with a resolver that finds the file, or remove the clips.

Any other failure, such as an unknown effect type, a wrong `version`, or a full storage, rejects. The files written so far are deleted, and the project stays exactly as it was.

## Document versions

`version` is 1 today. `migrateDocument(document)` brings an older document up to the current format and returns it; `loadDocument` calls it for you. A document with another version is refused.

## The built-in zip

Without handlers, the editor's "Save project" writes one zip and "Open project" reads one. `writeProjectZip(document, media)` returns a `Blob`. The zip is stored uncompressed. It points at the media files instead of copying them, and reads each file once for its checksum. Files and archives past 4 GB use ZIP64\. `readProjectZip(blob)` returns `{ document, resolve }`; each media file comes back as a slice of the zip. A zip re-packed with compression is refused.

```ts
import { readProjectZip, writeProjectZip } from "@vidstudio/editor";
import type { EditorBackend } from "@vidstudio/engine";

export async function roundTrip(backend: EditorBackend): Promise<Blob> {
  const { document, media } = await backend.saveDocument();
  const zip = await writeProjectZip(document, media);
  const opened = await readProjectZip(zip);
  await backend.loadDocument(opened.document, opened.resolve);
  return zip;
}

```

## The editor's handlers

`onSaveDocument(document, media)` replaces the zip download. `onOpenDocument()` replaces the zip picker and returns `{ document, resolve }`, or `null` when the user cancelled. An error thrown by either becomes an error toast. `features={{ projectFiles: false }}` hides both menu entries.

## Save to a server

This example uploads each file once, keyed by its hash, then stores the document with the asset-to-hash map. Save it as `upload-project.ts`.

```ts
import type { MediaManifest, ProjectDocument } from "@vidstudio/engine";

async function serverHas(hash: string): Promise<boolean> {
  const response = await fetch(`/api/media/${encodeURIComponent(hash)}`, { method: "HEAD" });
  return response.ok;
}

export async function uploadProject(key: string, document: ProjectDocument, media: MediaManifest): Promise<void> {
  for (const item of media) {
    if (await serverHas(item.hash)) continue;
    // read() is a handle; fetch streams the file from disk.
    const response = await fetch(`/api/media/${encodeURIComponent(item.hash)}`, { method: "PUT", body: await item.read() });
    if (!response.ok) throw new Error(`Uploading ${item.name} failed: ${response.status}`);
  }
  const hashes = Object.fromEntries(media.map((item) => [item.assetId, item.hash]));
  const response = await fetch(`/api/projects/${encodeURIComponent(key)}`, {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ document, hashes }),
  });
  if (!response.ok) throw new Error(`Saving the project failed: ${response.status}`);
}

```

The server keeps `{ document, hashes }` per project and one file per hash.

## Load from a server

This example fetches the document and resolves each asset to its file's web address. Save it as `fetch-project.ts`.

```ts
import type { MediaResolver, ProjectDocument } from "@vidstudio/engine";

export async function fetchProject(key: string): Promise<{ document: ProjectDocument; resolve: MediaResolver }> {
  const response = await fetch(`/api/projects/${encodeURIComponent(key)}`);
  if (!response.ok) throw new Error(`Loading the project failed: ${response.status}`);
  const saved = (await response.json()) as { document: ProjectDocument; hashes: Record<string, string> };
  const resolve: MediaResolver = (asset) => {
    const hash = saved.hashes[asset.id];
    // Throwing marks this one asset missing; the rest of the project still loads.
    if (!hash) throw new Error(`${asset.filename} was never uploaded`);
    return `/api/media/${encodeURIComponent(hash)}`;
  };
  return { document: saved.document, resolve };
}

```

Wire both into the editor, or call them around the backend.

```tsx
import { VidstudioEditor, createStore } from "@vidstudio/editor";
import "@vidstudio/editor/editor.css";
import { uploadProject } from "./upload-project";
import { fetchProject } from "./fetch-project";

type Store = Awaited<ReturnType<typeof createStore>>;

export function ServerBackedEditor({ store, projectId }: { store: Store; projectId: string }) {
  return (
    <div style={{ height: "100vh" }}>
      <VidstudioEditor
        store={store}
        projectId={projectId}
        onSaveDocument={(document, media) => uploadProject(projectId, document, media)}
        onOpenDocument={() => fetchProject(projectId)}
      />
    </div>
  );
}

```

With the backend alone: `const saved = await backend.saveDocument(); await uploadProject(key, saved.document, saved.media);` to save, and `const opened = await fetchProject(key); await backend.loadDocument(opened.document, opened.resolve);` to load.

---
Source: [https://vidstudio.app/developers/save-and-load](https://vidstudio.app/developers/save-and-load)
