Team Vault
unlistedby krem.digital
Sync your vault with a self-hosted server and share notes with your team. Edits from different devices merge instead of overwriting each other.
Team Vault — Plugin
Self-hosted vault synchronization for Obsidian: edits to the same note from different devices merge through a Yjs CRDT, offline edits catch up on reconnect, and every file keeps its version history. Companion to the Team Vault server.
- Plugin (this repo): https://github.com/kremdigital/TeamVaultPlugin
- Server: https://github.com/kremdigital/TeamVaultServer
Status: MVP, desktop-only. Mobile support deferred.
What it does
- Text notes merge instead of overwriting — when Obsidian saves a note (about two seconds after you type), the plugin folds the change into the note's Yjs CRDT document and sends it to the server; other devices merge it with their own edits and write the result to disk within a few seconds. Edits to the same note from different devices are combined without conflict prompts. Changes travel save by save, not keystroke by keystroke — see Limitations.
- Binary files — versioned via REST snapshots. A three-way conflict prompt opens when both sides have diverged from the same starting hash (keep-server / keep-local / keep-both).
- Offline-first — edits made while disconnected, or while sync is paused, queue in a local operation log; the engine drains the queue on reconnect with exponential-backoff reconnect to the server. Every change to a file goes out under an operation id that is written to the log before it is sent, and the server applies each id once: when an answer is lost (the connection drops, sync is paused, Obsidian quits or crashes), the next connect asks the server what it applied and sends only the rest.
- External edits — chokidar watches the filesystem, so changes from CLI scripts and AI agents propagate the same way as in-app edits.
- Folders follow their files — the server keeps files, not folders: a folder reaches a teammate with its first file. A folder you delete, rename or move goes from your teammates' vaults once nothing is left in it there (a file Team Vault doesn't sync keeps it). A folder you empty by deleting or moving its last file stays, for you and for them, and an empty folder you create stays on your device.
- Version history — right-pane view shows every server-side version of the active file, with author and timestamp.
Stack
- TypeScript 5.9, esbuild →
main.js(CJS, single bundle — no native modules, nothing to install alongside it) - Yjs + y-indexeddb for CRDT
- socket.io-client for live transport
- chokidar for the filesystem watcher
- Obsidian 1.7.2+ (desktop only —
isDesktopOnly: true, the watcher and the log need Node APIs that mobile doesn't have) - Jest + ts-jest, 500+ tests
Network use and privacy
Team Vault is the client half of a self-hosted setup, so it does talk to a server — here is exactly what that means:
- The only remote host contacted is the server URL you enter yourself. There is no vendor backend, no default endpoint, no fallback host. Nothing leaves your machine until you add a server and bind the vault.
- An account on that server is required, in the form of an API key you
generate in its web UI. The key is stored in plain text in the plugin's
data.json, inside your vault's config folder — keep that folder out of git and out of other sync tools. Team Vault never syncs the config folder itself, whatever the server asks for. - Synced content is the content of the bound vault — note text, binary
attachments, paths, and their edit history — sent to your server so other
devices and teammates can receive it. The config folder,
.trash,.gitand the service files of the OS and other sync tools are never synced (see What is never synced). - No telemetry, no analytics, no crash reporting. The plugin sends nothing anywhere else, and collects nothing about you.
- Logs stay local, in
.obsidian/plugins/team-vault/sync.log.
The server is open source and self-hosted: kremdigital/TeamVaultServer.
Installation
Install manually until the directory listing lands:
- Download
main.js,manifest.jsonandstyles.cssfrom the latest release on GitHub. - Copy them into
<your-vault>/.obsidian/plugins/team-vault/. - In Obsidian → Settings → Community plugins, enable "Team Vault".
- Open the Team Vault tab in Settings to add a server.
Those three files are all there is — since 0.3.0 the plugin has no native
dependencies, so there is no node_modules to install next to it.
Server version. Update the Team Vault server first, then the plugin on
every device. Team Vault 0.4.1 needs a server from the same release: one
that passes on which folder went with a delete, rename or move (folder),
so that a folder a teammate deleted or renamed goes from the other devices
too. With the server of 0.4.0, 0.4.1 still syncs, but such folders stay
behind, empty, on the other devices. To a server without operation ids (it
says so when a vault joins a project: opIdempotency in the answer) the
plugin sends nothing: the status bar shows an error, the notice and
sync.log say server_outdated (with changes waiting in the queue, after
about a minute and a half), and your changes wait in the offline queue until
the server is updated. Plugin versions before 0.4.0 are not supported: the
server refuses their new files, renames, moves, deletes and attachment
uploads.
Configuration
1. Get an API key from the server
In the web UI of your Team Vault server (e.g. https://sync.example.com):
- Sign in.
- Go to API Keys in the sidebar.
- Click Create new key, give it a name (e.g. "Laptop"), and copy the
shown
osk_…value. You won't see it again.
2. Add the server to the plugin
In Obsidian → Settings → Team Vault → Servers:
- Click Add server.
- Enter a display name, the server URL (
https://...), and the API key. - Click Test — the modal verifies the key by calling
GET /api/auth/meand prints the matching email on success. - Click Save.
Editing a server
To change a server's name, URL or API key, click Edit next to it. The form opens with the name and URL filled in. The API key field is empty: leave it empty to keep the current key (the form never shows it), or paste a new one. A new URL or key needs a passing Test before Save, as when adding a server; a new name alone doesn't. When vaults are bound to the server, a passing Test also checks that the server lists their projects, and a notice names any it doesn't.
The server keeps its bindings. On Save, the vaults bound to it reconnect to the new address at once — or on Resume sync, if sync is paused — with their unsent offline changes and offline documents: nothing syncs from scratch. Change the URL only when the same server has moved to a new address. Another server has projects of its own, and a vault bound here would stop syncing there with "project not found".
3. Bind the vault to a project
- Make sure the project exists on the server (create it via the web UI; the plugin doesn't create projects).
- In Team Vault → Vaults, click Add binding.
- Pick the server and the project. The whole vault is synced — the folder Obsidian opens is the one you mean, so there is nothing else to choose.
- Click Bind.
A vault holds one binding. To sync it with a different project, remove the current binding first. Bindings made by older versions to a subfolder keep working as they are — but once removed, a subfolder can't be bound again: the new binding covers the whole vault and uploads everything in it to the project.
The plugin connects to the server, pulls the file list, and starts synchronizing. The status bar shows the aggregate state.
4. Behavior settings
- Interface language (default Same as Obsidian) — the plugin's settings, notices and status bar follow Obsidian's own language: Russian when Obsidian runs in Russian, English otherwise. Русский or English pins one. The settings tab switches at once; command names in the command palette follow after the plugin is reloaded (turn it off and on in Community plugins, or restart Obsidian).
- Change debounce (default 500 ms) — how long to wait after a file is saved before pushing the change upstream. Higher = fewer round-trips but laggier remote view.
- Notifications — toasts for connect / disconnect / sync completion. Errors, conflicts and a note whose name can't be synced (see What is never synced) are always notified regardless.
- Log level — Errors only, Warnings, Info (default) or
Debug: what goes into
sync.log. At Debug every entry is also mirrored to the DevTools console (Ctrl+Shift+I). A change applies at once, no reload needed. - Event log — Open log shows
sync.login a window, with Copy (to the clipboard) and Clear log buttons; Clear log next to it empties the log without opening it. Nothing is written into the vault: the log lives in the plugin's own folder and is never synced.
There is no "sync on startup" switch: every binding catches up with the server whenever it connects, including at startup.
What is never synced
Some paths are skipped in both directions: they are never uploaded, and a
copy that arrives from the server is neither written to disk nor allowed to
delete or overwrite the local file. A path is skipped when any folder or file
name in it matches — except your vault's config folder, which is matched at
the vault root only, the one place Obsidian reads it from. Names are compared
the way a disk that ignores letter case compares them: .OBSIDIAN matches,
and so does a spelling macOS opens as the same folder, such as .obſidian
with a long s.
-
Obsidian and Team Vault — your vault's config folder at the vault root (
.obsidian, or the one you set in Obsidian), a folder named.obsidiananywhere (another vault's settings), Obsidian's.trash,.git, and the server's own.versions/.staging. -
Temporary files — names ending in
.tmpor~, and the leftovers of Obsidian's interrupted saves (<name>.tmp.<pid>.<hex>). A folder with such a name is skipped with everything in it. -
Editors — the owner files Word, Excel and PowerPoint keep next to an open document (
~$<name>), LibreOffice's lock files (.~lock.<name>#), Vim's swap files (.<name>.swp) and Emacs' lock files (.#<name>). -
macOS —
.DS_Store, AppleDouble files (._<name>), the folder icon fileIcon\r(its name ends in a carriage return), iCloud placeholders (.<name>.icloud), netatalk's.AppleDoublefolders, and what macOS keeps at the root of a volume:.Spotlight-V100,.fseventsd,.Trashes,.TemporaryItems,.DocumentRevisions-V100. -
Windows —
desktop.ini,Thumbs.db,$RECYCLE.BIN,System Volume Information. -
Linux —
.directory(Dolphin) and.Trash-<uid>folders. -
Other sync tools — Syncthing's
.stfolder,.stversionsand.stignore; Resilio Sync's.syncfolder and unfinished*.!syncdownloads; Dropbox's.dropbox,.dropbox.attrand.dropbox.cache; Google Drive's.tmp.drivedownloadand.tmp.driveupload; the Nextcloud and ownCloud client's journal (.sync_<hex>.dbwith its-wal/-shmfiles) and.owncloudsync.log. -
Names Windows can't keep as spelled — on Windows such a name opens a different file or folder than the one it spells, or can't be written at all:
- any name with a
:in it (Obsidian doesn't allow one either; on Windowsdesktop.ini::$DATAisdesktop.iniitself); - names shaped like a Windows short name: up to 8 characters ending in
~and digits, optionally with an extension of up to 3 (PROJEC~1,Draft~1.md). Windows can store such a name, but it may also be the short name of another file or folder, which it then opens instead; - names ending in a dot or a space (a folder
Notes.orNotes): Windows drops the dot or space, so deleting such a folder there could delete the folderNotesnext to it instead; - names with
*,?,<,>,",|or a control character (a noteWhy?.md): Windows can't store them.
Obsidian itself refuses
:everywhere, and on Windows also*,?,<,>,",|and a name ending in a dot or a space; on macOS and Linux it lets you create those. None of these names is synced on any system — not even between two Macs — so that no teammate uploads a name another teammate's disk can't hold. Unlike the rest of this list, such a name is often a note you meant to share, so when you create, edit or rename one in Obsidian, a notice names it and what is wrong with it, andsync.loggets awarnline for it (unless Log level is Errors only). A notice comes once per name while the plugin runs, wherever the name turns up; names reported within a moment of each other (a link update across many notes, a folder copied into the vault) share one notice, andsync.loglists each of them. A notice comes no sooner than 15 seconds after the one before, about when that one goes by itself, and takes in what was reported meanwhile. A rename that keeps the name at fault — renaming the folder above such a note, or moving the note within its binding — isn't reported, since nothing changes for your teammates; moving it into the folder of another binding (one an older version made) is. Renaming or moving a synced note to such a name works like deleting it for your teammates: a notice says so for every such rename, even if the name was reported before or the same note was renamed so earlier (renames close together share one notice), andsync.loggets a line for each, naming the note it was.Rename the note to sync it. A note an older version already synced under such a name is still on the server: rename it in the project's web interface (or have an MCP agent do it), and teammates get it under the new name, history included. Renamed in Obsidian, the note is uploaded as a new one, and the old one stays on the server, where teammates on older versions still see it, until someone deletes it there. The same goes for a folder with such a name: rename it, or the notes in it, in the web interface or through MCP.
After such a rename in the web interface, teammates on an older version see the note renamed. Anyone with this version who already has a copy under the old name keeps it next to the renamed note, no longer synced. For a name with
:,*,?,<,>,",|or a control character, that is anyone on a Mac or Linux: Windows can't store such a name. For a name ending in a dot or a space, or a short name such asDraft~1.md, it is anyone at all, Windows included: an older version wrote such names there as spelled. Everyone who has such a copy — you included — deletes it, after copying over any edits made in it since the update, which never reached the server. The notice about such a copy says so too.On Windows, take care deleting a copy whose name ends in a dot or a space. File Explorer, a plain
rdordel, and Obsidian with Deleted files (Settings → Files and links) at Move to system trash, the default, all drop the dot or space and delete the file or folder named without it: the synced folderNotesinstead of the copyNotes., and the plugin then deletesNotesfor the whole team. Set Deleted files to Move to Obsidian trash or Permanently delete before deleting such a copy in Obsidian, or delete it from a command prompt by its full path with the\\?\prefix, which keeps the name as spelled:rd /s "\\?\C:\Vault\Notes."for a folder,del "\\?\C:\Vault\Plan.md."for a file. - any name with a
The list is built in and the same on every device, so there is no setting for it. Such files that an older version already uploaded stay on the server; the plugin leaves them alone, and your own copy stays on your disk without syncing any more. Renaming a note to a name on the list works like moving it to the trash: teammates see it deleted, and your copy stays on your disk only.
Commands
Available from the command palette (Ctrl/Cmd-P), where Obsidian lists
them under the plugin's name:
- Team Vault: Sync now — runs a deep diff against every active binding (catches files that drifted while the plugin was offline). Not shown while paused.
- Team Vault: Pause sync — work offline on purpose. Team Vault disconnects from the server and stays disconnected until you resume, even when the network is up; a transfer under way is called off. Everything you do in Obsidian meanwhile — new notes and attachments, edits, renames, moves, deletes — is recorded as it is when the connection drops, and goes to the server when you resume: a note renamed several times goes as one rename, and a note deleted stays deleted, unless a teammate changed it meanwhile — then it comes back with their changes. Pause lasts until you resume or the plugin starts again (Obsidian restarted, the plugin turned off and on), which connects. Shown only while sync runs.
- Team Vault: Resume sync — connect again after a pause and send what you did meanwhile. Shown only while paused.
- Team Vault: Toggle active file history — opens the right-pane history view for the file currently in focus, or closes it if it is open.
- Team Vault: Open settings — focuses the plugin's settings tab.
With the interface in Russian the names are Russian too, e.g. Team Vault: Синхронизировать сейчас.
Status bar
The status bar widget reports the aggregate state across every active binding:
| Icon | State | Meaning |
|---|---|---|
check-circle | connected | Every binding is connected and up to date. |
refresh-cw | syncing | At least one binding is mid-sync. |
refresh-cw | connecting | Initial handshake in progress. |
pause | paused | You pressed Pause; changes wait for Resume. |
wifi-off | offline | Every binding lost the server. |
alert-circle | error | One or more bindings hit an error (see log). |
circle | idle | No active bindings yet. |
Click the widget for an action menu: Sync now and Pause — or only Resume while paused — then Active file history and Open settings.
Troubleshooting
"Test" fails with "Invalid API key" — the key was rotated server-side, or you copied an extra space. Generate a fresh one in the web UI.
"Test" fails with a network error — confirm the URL (no trailing slash
needed; the plugin trims it) and that the server is reachable from this
machine. Try curl -H "X-API-Key: osk_…" https://your-server/api/auth/me.
The server moved to a new address — click Edit next to the server in
Team Vault → Servers, enter the new URL, click Test, then Save
(see Editing a server). Don't remove the server and
add it again: removing it switches its bindings off, and a binding made again
starts from scratch — the unsent offline changes stay behind with the old
binding. Nor is there any need to edit data.json by hand any more.
"Error: server_outdated" — the server is older than the plugin and doesn't keep operation ids (see Installation). Nothing is sent until the server is updated; your changes wait in the offline queue and go out at the next connect after the update. With changes waiting, the error comes after about a minute and a half: the plugin first asks the server about them, an older server never answers, and the plugin then checks the server's version.
"Error: ops_status_failed" — the server is up to date but couldn't answer the question the plugin asks at each connect about changes whose answers were lost: it was busy or silent for four tries, or failed on its own (its database, say). Nothing is lost or sent twice: the plugin asks again at the next connect (Pause sync and Resume sync make one). Once the server has failed to load the project, Team Vault asks again by itself instead (see the next entry). If it keeps failing, check the server's logs and that it is running.
"Error: the server could not load the project; Team Vault will try again on
its own" — the server couldn't read what connecting to the project needs,
usually because of its database. Team Vault tries again by itself, without
reconnecting, after 2, 5, 15, 30 and then every 60 seconds. While the
database stays down, the other steps of connecting fail too — the list of the
project's files (an HTTP 5xx answer, or the server not reached), the check of
changes waiting in the queue (an error of the server's own, or busy or no
answer four times over) — and Team Vault keeps trying through them, with the
same message. sync.log says the server could not answer for now; joining again at each try, with the step that failed and its error. It stops trying
only when the server refuses for good (the project was deleted or your access
to it removed, say) or another error ends the connect: the status bar's
tooltip then shows that error, no new notice comes, and Pause sync and
Resume sync connect again. Meanwhile your file changes wait in the offline
queue and go out once the vault has caught up. If it keeps failing, check the
server's logs and its database.
Status stays at connecting… — the plugin handles the WebSocket
upgrade; if your reverse proxy doesn't pass Upgrade / Connection
headers cleanly, the socket can't establish. Check Caddy / nginx logs.
Files don't sync — open the log via Settings → Team Vault → Behavior →
Open log.
Look for [error] lines. Common causes:
- The binding is switched off (toggle in Team Vault → Vaults).
- The file's name is on the built-in ignore list — see
What is never synced. Local files like that are
skipped without a log line, except a name Windows can't keep: when you
create, edit or rename such a note, it gets a notice and a
warnlinenot synced: Windows cannot keep this name as spelled. One the server still holds is logged asrefused a path supplied by the serverwith"reason":"ignored"(or"invalid"for a name Windows can't keep as spelled) — atwarnonce per path while the plugin runs, and atdebugafter that. With Log level at Errors only neitherwarnline is written; raise the level, then edit the note (for a path from the server, run Pause sync and Resume sync) to see it. - A binding made by an older version points to a subfolder that no longer exists — remove it and bind the vault again. Note that the new binding covers the whole vault, so every note in it goes to the project.
External agent edits don't propagate — the FS watcher uses chokidar
on the vault root and respects the same ignore list: the config folder,
.trash, .git, the server's own .versions / .staging, temporary files,
editors' lock and swap files, and the service files of the OS and other sync
tools (.DS_Store, desktop.ini, Thumbs.db, .stfolder, … — the full list
is in What is never synced). For a binding made to a
subfolder by an older version, files outside that folder are correctly
ignored.
"The plugin settings file is damaged" notice — the plugin's data.json
(in .obsidian/plugins/team-vault/) isn't valid JSON any more, usually
after a hand edit. The plugin doesn't start rather than overwrite it. Fix the
file (a stray comma is the usual culprit; sync.log has the parser's error
with its position) or restore it from a backup, then turn the plugin off and
on in Settings → Community plugins. Don't delete the file: that resets the
plugin — servers, keys and bindings — and its unsent offline changes are
dropped.
"Could not open the plugin settings file" notice — another program (an
antivirus, a cloud-sync client) kept data.json locked while the plugin
started. Close it or wait a moment, then turn the plugin off and on.
"Part of the plugin settings file could not be read" notice — data.json
is valid JSON, but a server or a binding in it lacks a field the plugin needs
(a binding's id, serverId or projectId; a server's id, url or
apiKey) or isn't the right shape, usually after a hand edit. The plugin
starts with the rest. The skipped entries don't sync, but they stay in the
file as they are, and their unsent offline changes are kept: the start-up
cleanup of leftover local state is off until they are fixed. sync.log names
each one, whatever the log level — its position in the list, its id and the
fields at fault. While a binding is skipped, Add binding stays disabled:
the skipped one may be this vault's binding, and a vault holds only one. To
fix or remove the entries, first turn the plugin off in Settings → Community
plugins (or quit Obsidian) — a running plugin saves its settings over your
edit — then edit data.json and turn the plugin back on.
"This vault now syncs as a new device" notice — each copy of a vault
syncs under a client id of its own, which keeps its changes apart from other
devices' on the server. The id is in the plugin's data.json, which a copy
of the vault takes along (a USB stick, a cloud drive, git), so Team Vault
also keeps the id in Obsidian's own storage for this vault, which a copy
doesn't take along. A vault whose data.json has an id already kept for
some vault, while Obsidian's storage for this vault holds none, gets a new one
on its first start: a copy, but also a vault moved to another folder, one
removed from Obsidian's vault list and opened again, or one whose Obsidian
data was cleared. A vault whose storage holds an id syncs under that one,
whatever data.json says. Nothing needs to be done: notes, bindings and
unsent changes stay as they are, and the vault just counts as one more
device on the server. sync.log has a client id replaced line with the
reason.
sync.log says "another device uses the same id" — Team Vault saw a
change under this vault's client id that this vault didn't send, from a copy
made before Team Vault 0.4.1 or one that came with Obsidian's own data (a
whole user profile or disk copied, macOS Migration Assistant). It applies the
other device's changes as a teammate's, and gives this vault a new id the
next time Obsidian starts (see the notice above). Nothing needs to be done:
don't edit "clientId" in data.json by hand. The other device's new changes
reach this vault; ones it made while this vault was offline may have been
missed, so check a file it changed that looks out of date against its
versions (the history view or the web UI). This change of id happens at most
once a day: a vault whose id changed less than a day ago keeps it until its
first start after that, and sync.log says so. A copy is never held off: it
gets its own id on its first start.
Conflict modal keeps showing — happens for binary files when both
sides changed since the last sync. Pick "Keep server" if you trust the
server's copy, "Keep local" to push yours, or "Keep both" to keep your
edits in …conflict-<ts>.<ext>.
Limitations (MVP)
- No live co-editing inside Obsidian's editor: changes travel when Obsidian saves the note, not keystroke by keystroke, and there are no remote cursors. If a teammate's change arrives while you have unsaved typing in that note, Obsidian merges your typing into it and shows a "modified externally" notice; if you both changed the same words, part of your unsaved typing can be lost. To type together in real time, use the web editor of the Team Vault server.
- Desktop only: the filesystem watcher and the log both use Node APIs that Obsidian mobile doesn't expose.
- Don't keep a bound vault in iCloud Drive with Optimize Mac Storage on
macOS 13 or earlier. To free space, macOS swaps a note for a
.<name>.icloudplaceholder. Team Vault reads that as the note being deleted and deletes it for the whole team. Move the vault out of iCloud Drive or turn the optimization off. - Names that differ only in letter case (
A.mdnext toa.md,Notes/andnotes/) are two files on the server and on Linux, but one on Windows and macOS. When a teammate on Linux, the web editor or MCP makes such a second name, it doesn't come to a Windows or Mac vault while the first one is there; neither file's text changes. A folder a teammate renames only in letter case keeps its old case on a Windows or Mac disk: its notes stay synced, and a note you create in it goes to the server under the folder's old case. - Team Vault syncs a rename or a delete only when it sees it happen: in
Obsidian, or on disk while Obsidian runs with the plugin on and the file's
binding switched on — online, offline or paused. A file renamed or moved
while Obsidian is closed, the plugin is off or the binding is switched off
reaches your teammates as a new file next to the old one. A note's old
name comes back to your vault too; an attachment's doesn't, so only your
teammates have both. A file deleted then stays on the server, and a note
comes back to your vault. A rename made outside Obsidian while it runs (in
Explorer or Finder, by
gitor a script) reaches them as the old file deleted and a new one created, and a note's version history stays with the deleted one. So rename, move and delete files in Obsidian, and to keep working without syncing, use Pause sync rather than closing Obsidian, turning the plugin off or switching the binding off. - A folder a teammate deleted or renamed stays behind, empty, on a device that was away so long that the server sent it only the newest changes, and the folder's were not among them, and on one whose Team Vault or server predates this; delete it by hand.
- Project creation is server-only — the plugin binds to existing projects.
- The conflict modal is bare-bones (no image preview, no inline diff).
- The history view is read-only — restoring a version requires the web UI.
- One client id per copy of a vault (vector clock keys must be unique per
device). A copy that takes the plugin's
data.jsonalong gets an id of its own on its first start; so does a vault moved to another folder, which is harmless (see "This vault now syncs as a new device" under Troubleshooting). A copy made together with Obsidian's own data (a whole user profile or disk) is found only once it syncs a change, and gets its id at the next start after that. Each new id stays in the project's vector clocks for good and makes catch-ups a little slower, so don't let the plugin folder be copied back and forth. - Don't sync
.obsidian/plugins/team-vault/between computers with another tool (Obsidian Sync with "Installed community plugins" on, Syncthing, Dropbox, a cloud drive): each computer keeps its own id — the one in Obsidian's storage for its vault, whatever the shareddata.jsonsays — and rewrites the shareddata.jsonon every start, and a sharedstate.jsonholds the other computer's unsent changes, which would then be sent twice. - Two copies of a vault on one computer share the offline history of their notes (Obsidian keeps it once per computer, by binding). Don't open both at once, and don't remove the binding in the copy: that deletes the offline history the original uses too. To get rid of a copy, delete its folder and remove it from Obsidian's vault list.
- Going back to Team Vault 0.4.0 drops the client-id fields from
data.json. If you then set"clientId"to""there by hand, as the 0.4.0 README advised, the next 0.4.1 start takes back the id this vault had before, which it still keeps in Obsidian's storage.
Development
CI=1 pnpm install --frozen-lockfile
pnpm build:vault # production main.js + copy the release files to TEST_VAULT
pnpm dev # esbuild --watch, unminified, inline sourcemap
pnpm test # Jest, 500+ tests
pnpm typecheck
pnpm lint # ESLint + eslint-plugin-obsidianmd, the directory's review rules
pnpm build # production main.js
pnpm cli help # protocol-debug CLI emulator
pnpm build:vault copies main.js, manifest.json and styles.css into
$TEST_VAULT/.obsidian/plugins/team-vault/, so TEST_VAULT must be set to
the root of a test vault and that plugin folder must exist. The build reads
the variable from the environment only — it doesn't load .env files. Set it
in the shell (TEST_VAULT="/path/to/vault" pnpm build:vault) or pass a
.env to Node yourself; .env.example shows both.
See CONTRIBUTING.md for the gates a change has to pass and the commit conventions.
Layout
src/
main.ts # plugin entry — wires every subsystem together
settings/ # settings types + tab UI + add-server / add-binding modals
client/ # REST + Socket.IO clients
sync/ # operation-log, vector-clock, engine, engine-manager,
# conflict, reconnect, hash, file-type, vault-adapter
crdt/ # Y.Doc cache, text-diff helper
watcher/ # ObsidianWatcher + FsWatcher + path utilities
ui/ # status bar, commands, notice service, history view,
# conflict modal
integration/ # concrete Obsidian adapters (vault, log storage,
# watchable vault)
i18n/ # ru / en catalogs, tiny `t()` helper, language pick
utils/ # logger, debounce, hash, uuid
scripts/
cli-emulator.ts # CLI-based protocol debugger
version-bump.mjs # `pnpm version` hook: manifest.json + versions.json
release-notes.mjs # one version's CHANGELOG section → GitHub release body
bundle-licenses.mjs # license notices of bundled packages → end of main.js
tests/ # Jest, organized 1:1 with src
Security
Found a vulnerability? Please report it privately — see SECURITY.md — rather than in a public issue.
License
MIT — see LICENSE.
main.js bundles third-party packages — Yjs, lib0, y-indexeddb,
socket.io-client, chokidar, diff and their dependencies — under their own
licenses (MIT, BSD-3-Clause). The build appends each one's name, version and
full license text to the end of main.js, so the notices ship with every
install.
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.