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.
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.
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
setupreturns runs last inclose(), after the final saves. - Under Vite,
setupruns 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.
| Change | Reported |
|---|---|
| a save | after the write |
| a file tree command | for each file it writes or deletes |
| a collab edit | at 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: diris shorthand forstorage: 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.
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 anode_modulessegment 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:
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