Fumadocs Editor

Sync Server

Run the sync server, reach it from your code and choose where files live

The sync server holds the files, checks permissions and answers the editors. You run it in one of three ways, and they all take the same options.

Run it

The Vite plugin runs the server inside vite dev. root resolves against the Vite project root.

vite.config.ts
import { defineConfig } from "vite";
import { editorSync } from "@fumadocs-editor/core/vite";

export default defineConfig({
  plugins: [editorSync({ root: "content/docs" })],
});

On every dev server restart, the plugin closes the server and creates a new one.

Options

Prop

Type

Reach the server with setup

setup is called with the server once it is created, in every integration. Use it to listen to changes and to keep the server for later.

vite.config.ts
import { defineConfig } from "vite";
import { editorSync } from "@fumadocs-editor/core/vite";

declare function record(entry: { user?: string; path: string; at: Date }): void;

export default defineConfig({
  plugins: [
    editorSync({
      root: "content/docs",
      setup(sync) {
        // returns the unsubscribe, which runs when the server closes
        return sync.onChange(({ path, connection }) => {
          record({ user: connection.user?.name, path, at: new Date() });
        });
      },
    }),
  ],
});
  • What setup returns runs last in close(), after the final saves.
  • Under Vite, setup runs again for each new server after a restart, so keep per-server state inside it.

Open connections

sync.connections() lists the authenticated connections open now:

Prop

Type

close() ends a connection at once. The client reconnects, and authenticate decides again. See when access changes.

Changes

sync.onChange reports each file a connection changed, once it is written. It returns the unsubscribe.

ChangeReported
a saveafter the write
a file tree commandfor each file it writes or deletes
a collab editat the debounced write, once per connection whose edits it carries

Changes made around the server, like an IDE save, have no connection and are not reported.

Storage

By default the files live in the root directory. Pass storage instead to keep them in a database, a bucket or anywhere else.

  • root: dir is shorthand for storage: fsStorage(dir).
  • Everything goes through the storage: documents, the file tree, uploads and assets.
  • A plain key-value store is enough. Versions, locks and the tree stay in the server.
storage.ts
import { createSyncServer, type SyncStorage } from "@fumadocs-editor/core/node";

declare const db: {
  keys(prefix: string): Promise<string[]>;
  get(key: string): Promise<Uint8Array | undefined>;
  set(key: string, value: Uint8Array): Promise<void>;
  delete(key: string): Promise<void>;
};

const storage: SyncStorage = {
  name: "Docs",
  list: (dir) => db.keys(dir ? `${dir}/` : ""),
  read: (path) => db.get(path),
  write: (path, data) => db.set(path, data),
  delete: (path) => db.delete(path),
};

export const sync = createSyncServer({ storage });

Prop

Type

Paths

The server normalizes every path before the storage sees it:

  • relative to the root, with / separators (guide/intro.mdx)
  • never outside the root
  • never hidden: a path with a dot segment (.env, .git/config) or a node_modules segment is refused everywhere, in documents, the tree and assets

Hidden files your list returns are skipped.

Watching

watch reports files changed around the server, like an IDE save or a git pull, so open editors merge them in.

Without watch, the server only sees its own writes. Saves still never overwrite a change made elsewhere, since each save reads the file first.

Wrap fsStorage

To do more with files on disk, spread fsStorage and override what you need:

storage.ts
import { fsStorage, type SyncStorage } from "@fumadocs-editor/core/node";

declare const backup: { put(path: string, data: Uint8Array): Promise<void> };

const disk = fsStorage("content/docs");

// every write also goes to a backup
export const storage: SyncStorage = {
  ...disk,
  async write(path, data) {
    await disk.write(path, data);
    await backup.put(path, data);
  },
};

Other backends

To replace the Node server itself, speak the sync protocol.

Last updated on

On this page