# Custom backend

> Write Fabric.js backend storage for your own REST API: revision checks in SQL, clear errors, image upload and versions, in one small adapter.

Source: https://fabricjs-document-engine.jscrate.dev/docs/storage/custom-backend
Last updated: 2026-09-28

Fabric.js backend storage is one object with two functions. Below is a full adapter for a REST API, and the server logic that keeps saving safe.

## The rules

1. Reject a save when the stored revision is not `expectedRevision`. Throw an error with `code: "SAVE_CONFLICT"`, or use `createConflictError`.
2. Skip that revision check when `expectedRevision` is `null`. The user chose to overwrite.
3. Throw an error with `retryable: false` when trying again cannot help, such as for a permission error.
4. Pass `signal` to `fetch`, so a save is cancelled when another document is opened.

## The adapter

```ts
import type { DocumentStorage } from "fabricjs-document-engine";

export const restStorage: DocumentStorage = {
  async loadDocument(id) {
    const response = await fetch(`/api/documents/${encodeURIComponent(id)}`);
    if (response.status === 404) {
      throw Object.assign(new Error("Not found"), {
        code: "DOCUMENT_NOT_FOUND",
      });
    }
    if (!response.ok) throw new Error(`Loading failed with ${response.status}`);
    return response.json();
  },

  async saveDocument(document, { expectedRevision, signal }) {
    const response = await fetch(
      `/api/documents/${encodeURIComponent(document.id)}`,
      {
        method: "PUT",
        signal,
        headers: {
          "Content-Type": "application/json",
          "If-Match":
            expectedRevision === null ? "*" : String(expectedRevision),
        },
        body: JSON.stringify(document),
      }
    );
    if (response.status === 409 || response.status === 412) {
      throw Object.assign(new Error("Someone else saved this document"), {
        code: "SAVE_CONFLICT",
      });
    }
    if ([401, 403, 422].includes(response.status)) {
      throw Object.assign(new Error(`Save refused with ${response.status}`), {
        retryable: false,
      });
    }
    if (!response.ok) throw new Error(`Save failed with ${response.status}`);
    const { revision } = (await response.json()) as { revision: number };
    return { revision };
  },
};
```

`document.revision` is the next revision. If your backend assigns its own number, return it as `{ revision }`.

## The server

Run the revision check and the write as one step, so two saves can never both pass. In SQL:

```sql
UPDATE documents
SET body = $1, revision = revision + 1
WHERE id = $2 AND ($3::int IS NULL OR revision = $3)
RETURNING revision;
```

If no row comes back, answer `409`. Checking first and writing second, in two queries, lets a second save slip in between.

## Image upload while saving

Images that exist only in the user's tab are uploaded once, and the document stores the returned URL:

```ts
const engine = createDocumentEngine({
  canvas,
  storage: restStorage,
  assets: {
    upload: async ({ blob }) => {
      const body = new FormData();
      body.append("file", blob);
      const response = await fetch("/api/uploads", { method: "POST", body });
      const { url } = (await response.json()) as { url: string };
      return url;
    },
  },
});
```

## Versions

To keep [versions](https://fabricjs-document-engine.jscrate.dev/docs/guides/versions) in your Fabric.js backend storage, add four methods to the adapter: `saveVersion(version)`, `listVersions(documentId)`, `loadVersion(documentId, versionId)` and `deleteVersion(documentId, versionId)`. `listVersions` returns summaries without the `document` field, newest first.

When the adapter is done, run [the adapter check](https://fabricjs-document-engine.jscrate.dev/docs/storage/verify-adapter).
