Fabric.js Document Engine

Search documentation

Find a page or section

中文

Production checklist

View as Markdown

Every feature together, the way a real editor uses them.

This guide puts every feature together for a Fabric.js editor in production: storage, autosave, recovery, versions, limits and assets, plus error handling for each failure.

The engine

import {
  bindKeyboardShortcuts,
  bindUnsavedChangesWarning,
  createDocumentEngine,
} from "fabricjs-document-engine";
import { createIndexedDbRecovery } from "fabricjs-document-engine/recovery";
 
export function createEditor(canvas: Canvas) {
  const engine = createDocumentEngine({
    canvas,
    storage: restStorage,
    customObjects: [{ fabricClass: Sticker, properties: ["label"] }],
    autosave: { delay: 1500, maxWait: 15000 },
    saveRetry: { attempts: 4, baseDelay: 800, maxDelay: 10000 },
    recovery: { store: createIndexedDbRecovery(), interval: 3000 },
    versions: { autoEvery: 20, keepAuto: 30 },
    history: { limit: 150, maxBytes: 48 * 1024 * 1024 },
    limits: {
      maxObjects: 20000,
      isAllowedUrl: (url) =>
        url.startsWith("https://cdn.example.com/") || url.startsWith("blob:"),
    },
    assets: {
      resolveUrl: (url) => signCdnUrl(url),
      upload: async ({ blob }) => uploadToBucket(blob),
      replaceMissingImage: () =>
        "https://cdn.example.com/placeholders/missing.png",
    },
  });
 
  const unbindShortcuts = bindKeyboardShortcuts(engine);
  const unbindWarning = bindUnsavedChangesWarning(engine);
 
  return {
    engine,
    dispose() {
      unbindShortcuts();
      unbindWarning();
      engine.destroy();
    },
  };
}

Opening a document

Check for an interrupted load and for recovery copies before opening:

async function openDocument(engine: DocumentEngine, id: string) {
  const interrupted = await engine.getInterruptedLoad();
  if (
    interrupted?.documentId === id &&
    !(await confirmUser("This document failed to open last time. Try again?"))
  ) {
    return;
  }
 
  const copy = await engine.getRecovery(id);
  if (copy && (await confirmUser("Restore unsaved work?"))) {
    await engine.restoreRecovery(id, { discardUnsavedChanges: true });
    return;
  }
  if (copy) await engine.discardRecovery(id);
 
  await engine.load(id);
}

Error handling

Every failure is a DocumentEngineError with a stable code. Switch on the code, never on the message:

engine.on("load:error", ({ error }) => {
  switch (error.code) {
    case "LOAD_ABORTED":
      return;
    case "MISSING_ASSETS":
      return showMissingImages(error.missingAssets);
    case "UNKNOWN_OBJECT_TYPE":
      return showMessage("This document needs a newer version of the editor.");
    case "UNSUPPORTED_SCHEMA":
      return showMessage(
        "This document was made with a newer version of the editor."
      );
    case "UNSAFE_DOCUMENT":
      return showMessage("This file was blocked for safety reasons.");
    default:
      return showMessage(`The document could not be opened: ${error.message}`);
  }
});
 
engine.on("save:error", ({ error }) => {
  if (error.code === "SAVE_CONFLICT") return askConflict();
  if (error.code !== "SAVE_CANCELLED") {
    showMessage("Saving failed. Your work is kept on this device.");
  }
});

Every code is listed in error codes.

Leaving the editor

async function closeEditor(editor: ReturnType<typeof createEditor>) {
  if (editor.engine.isDirty())
    await editor.engine.save().catch(() => undefined);
  editor.dispose();
}

destroy() also writes a recovery copy of unsaved work, so nothing is lost when that last save fails.

On the server with Node.js

The document helpers run in Node.js without a canvas. Check every upload before you store it, as shown in security.

Checklist

Before you launch a Fabric.js editor in production, go through this checklist:

  • verifyStorageAdapter(yourStorage) passes. See verify an adapter.
  • The server compares the revision and writes in one query.
  • Custom classes are registered everywhere documents are opened.
  • Web fonts load through assets.loadFont.
  • Images load with CORS and crossOrigin: "anonymous", so exports work.
  • You have UI for SAVE_CONFLICT, MISSING_ASSETS, recovery offers and UNSAVED_CHANGES.
  • Your toolbar follows the accessibility guide.

A Fabric.js editor in production that passes this list handles the failures users actually hit.