This is a custom replicator between Self-hosted LiveSync remote vaults and storage. The Unified Version of filesystem-livesync and livesync-classroom.
A Vault or storage can be synchronised with vaults or storage. You can even combine them. Of course, different passphrases for each vault could be used. And, you can synchronize documents under the specified folder on the vault, to another vault's specified one.
Of course, it is multi-directional!
- Deno is required.
Published Commonlib 0.3.1 provides the /storage entry, Unicode-resolving
storage adapters, and the direct-file pathNormalizationVersion option required
by this integration. This Bridge implementation remains unreleased and pins
that exact registry version in its import map and deno.lock.
Follow the Commonlib and application release order.
Each peer selects pathNormalizationVersion:
- omitted or
0retains Legacy path identity; 1selects Unicode NFC path identity; and- any other value is rejected.
Peers with the same group must select the same version before they start.
An omitted group is the shared empty group. Independent groups can select
different versions. The CouchDB database and its participating LiveSync clients
must use the selected version too. The bridge does not adopt another identity
silently from remote tweaks.
handleFilenameCaseSensitive remains separate from Unicode normalisation.
On storage peers it controls NFC name comparison; Legacy keeps its existing
native path behaviour. On CouchDB peers it controls the existing document-ID
case rule. Choose the same case rule as the participating LiveSync clients.
The default is false. Legacy and NFC groups are checked for agreement before
startup and before transfers. A CouchDB peer also compares its effective case
rule with the remote preferred setting before watching. An omitted remote case
value means false.
useRemoteTweaks: true explicitly adopts the remote settings, preferring the
PREFERRED entry and retaining the first entry as a fallback for older
milestones. The adopted case rule is checked against the group before the
changes feed starts. Adoption updates that CouchDB peer; it does not rewrite
storage peers or migrate document IDs.
Document-ID admission runs before remote tweaks are read. For a database with
obfuscated document IDs, set handleFilenameCaseSensitive to the database's
existing value in the initial peer configuration: useRemoteTweaks cannot
correct an initial case rule which fails that admission check. The mismatch
override below also retains this prerequisite.
For migration compatibility, the top-level disableCheckingConfigMismatch
option defaults to false. Set it to true to permit filename-case differences
within groups and against remote settings:
This override preserves each peer's effective case rule. It does not align
names, migrate data, bypass alias checks, or permit different
pathNormalizationVersion values in one group or against the database. Keep
case rules consistent for normal operation. Bridge currently uses this option
for filename-case checks; existing document-ID admission, encryption, and
compression checks retain their behaviour.
For example, an NFC group with case-sensitive names can use:
{
"peers": [
{
"name": "notes-files",
"type": "storage",
"group": "notes",
"baseDir": "./vault",
"pathNormalizationVersion": 1,
"handleFilenameCaseSensitive": true
},
{
"name": "notes-database",
"type": "couchdb",
"group": "notes",
"baseDir": "",
"url": "https://couchdb.example.com",
"database": "notes",
"username": "<username>",
"password": "<password>",
"passphrase": "",
"obfuscatePassphrase": "",
"pathNormalizationVersion": 1,
"handleFilenameCaseSensitive": true
}
]
}NFC storage resolves each existing file and parent using its actual directory name. Ordinary writes preserve those physical names; missing components retain the requested spelling. Multiple names which have the same NFC and case comparison key are ambiguous, including an exact-match request. Reads, writes, and deletions reject the ambiguity before changing a file. Symbolic-link components are not followed in NFC mode. Permission and I/O failures remain failed operations rather than evidence of absence or successful delivery.
Both Deno and Chokidar watchers retain event paths. An old deletion event does not remove a database entry while an equivalent live spelling remains. NFC inventories validate aliases before reporting the watcher healthy, and queued notifications from a stopped watcher are discarded. NFC file observations and CouchDB resume positions include the selected identity policy, so a Legacy observation or checkpoint cannot suppress the initial NFC read. Existing Legacy state remains available under its existing keys.
Changing this setting alone does not migrate an existing database. Preserve a
complete, authoritative Vault, unique local edits, and required history, stop
synchronisation on all clients, and use LiveSync's coordinated Rebuild and Fetch
procedure before reconnecting Bridge with the matching mode. The bridge does
not rewrite document IDs or merge histories in place. The existing
scanOfflineChanges option still controls whether the initial storage inventory
uploads local content.
- Clone the GitHub Repository
git clone https://github.com/vrtmrz/livesync-bridge
- Open the config file dat/config.sample.json, edit and save to dat/config.json. (You do not have to worry, the sample is in the following section).
- Simply run like this.
$ deno install
$ deno task runNote: If you want to scan all storage and databases from the beginning, please run with --reset.
CouchDB resume positions are saved separately for each peer name, baseDir,
server URL (including any proxy path), and database name. Credentials are not
part of this identity. Existing checkpoints without a remote identity are not
reused, so the first connection after upgrading reads remote changes from the
beginning. Keep the Deno localStorage data to retain resume positions across
restarts.
CouchDB changes with unavailable chunks are retried from the source database, while failed deliveries are retried with capped backoff and other files continue. A newer change for the same document supersedes its pending read so a stale result cannot restore older content. The resume position advances through completed changes only; later changes can be replayed after restart if an earlier one keeps failing. Retries keep pending change metadata in memory and read file content from CouchDB as needed.
- Clone the GitHub Repository
git clone https://github.com/vrtmrz/livesync-bridge
-
Open the config file dat/config.sample.json, edit and save to dat/config.json. (The storage folder has to start with "data/" to be in the volume)
-
Simply run like this.
docker compose up -dThe Compose configuration stores Deno's localStorage data, which offline
scans use to identify changed files, in the named volume
bridge_local_storage. Keep this volume when recreating containers. Running
docker compose down --volumes removes it, so the next offline scan must
rebuild this data.
The named volume is the default because it preserves the scan state across
container recreation without requiring a host path. You can replace it with a
bind mount if you prefer to manage this data on the host. Create a
docker-compose.local.yml file containing:
services:
bridge:
volumes:
- type: bind
source: ./dat/location_data
target: /deno-dir/location_data
bind:
create_host_path: falseCreate the source directory before starting the bridge:
mkdir -p dat/location_data
docker compose -f docker-compose.yml -f docker-compose.local.yml up -dOn Linux, ensure that the directory is writable by UID and GID 1993:1993,
which the current image uses for the deno user. Files below dat/ are
already excluded from this repository by .gitignore.
For host-managed temporary storage, use a path below /tmp instead, for
example:
source: /tmp/livesync-bridge-location_dataCreate this directory and give the deno user write access before starting
the bridge. Its lifetime depends on the host's /tmp clean-up policy; it can
remain after the container is removed.
For state which should last only as long as the container, replace the bind
mount above with a memory-backed tmpfs mount:
services:
bridge:
volumes:
- type: tmpfs
target: /deno-dir/location_data
tmpfs:
mode: 0o1777The contents of this mount are lost when the container stops or is recreated, so the next offline scan must rebuild the state.
The configuration file consists of the following structure. The basic example below leaves hidden-file synchronisation disabled. To opt in, see Synchronising internal/hidden files.
{
"peers": [
{
"type": "couchdb", // Type should be `couchdb or storage`
"name": "test1", // Should be unique
"group": "main", // we can omit this.
"database": "test",
"username": "admin",
"password": "password",
"url": "http://localhost:5984",
"customChunkSize": 100,
"minimumChunkSize": 20,
"passphrase": "passphrase", // E2EE passphrase, if you do not enabled, leave it blank.
"obfuscatePassphrase": "passphrase", // Path obfuscation passphrase. Leave blank to disable it; this may differ from passphrase.
"idDerivationKey": "sls-id-v1:...", // ID key of the vault, if it uses one (see below). Omit it otherwise.
"baseDir": "blog/", // Sharing folder
"useRemoteTweaks":true // Overwrite customChunkSize or minimumChunkSize, and check configuration matches
},
{
"type": "couchdb",
"name": "test2", // We can even synchronise the same databases as long as they have different names in here.
"group": "main", // we can omit this.
"database": "test2",
"username": "admin",
"passphrase": "passphrase",
"password": "password",
"url": "http://localhost:5984",
"customChunkSize": 100,
"minimumChunkSize": 20,
"obfuscatePassphrase": "passphrase",
"baseDir": "xxxx/",
},
{
"type": "storage",
"name": "storage-test1",
"group": "main", // we can omit this.
"baseDir": "./vault/", // The folder which have been synchronised.
"processor": { // The processor configuration. You can omit this.
"cmd": "script/test.sh", // The programme which run at file modification or deletion.
"args": [ "$filename", "$mode" ]
// The modified file is set to $filename. The mode is set to `deleted` or `modified`.
// $filename and $mode have been set also in environment variables.
},
"scanOfflineChanges": true,
"ignore": ["**/.git/**", "node_modules/**"], // Glob patterns which are never synchronised. Omit to synchronise everything.
"useChokidar":false, // Use Chokidar instead of Deno.watchFs; see filesystem watcher limitations.
}
]
}Storage peers use Deno.watchFs by default. Setting useChokidar to true
selects Chokidar instead.
On Windows, renaming a watched directory containing subdirectories can fail with an access-denied error when Chokidar uses its default event-based watching. This includes changing only the letter case of a directory name and moving it to another name in the same parent directory. This restriction affects normal use while the bridge is running, including filesystem operations initiated outside the bridge. Treat these directory-tree renames as unsupported with Chokidar's default watching mode on Windows.
Chokidar also offers polling through the CHOKIDAR_USEPOLLING=true environment
variable. This periodically checks filesystem state and can increase CPU use.
Its behaviour for these directory renames has not been verified in the bridge
on Windows. See the Chokidar 3.6 options
for the polling intervals and other trade-offs.
See the upstream Chokidar report
and Microsoft's directory rename constraints.
The default Deno.watchFs backend can report a directory being created or
renamed without reporting changes to files inside it. A file written
immediately after its directory is created, or a child file at its new path
after an ordinary or case-only directory rename, can therefore be missed while the
bridge is running. These behaviours were reproducible on Linux before the
internal-file synchronisation change. Switching to Chokidar does not guarantee
that all child-event gaps are resolved; its Windows directory-rename
restriction is described above.
Recent versions of Self-hosted LiveSync derive chunk IDs and obfuscated document IDs from a separate ID key. New vaults with E2EE get a random ID key by default. The bridge needs the same key, or the plugin cannot read the files the bridge writes. It then reports that not all files could be synchronised.
Copy the key on any device with Show current recovery code in the plugin settings, and set it as idDerivationKey:
{
"type": "couchdb",
"name": "test1",
// ...
"passphrase": "passphrase",
"obfuscatePassphrase": "passphrase",
"idDerivationKey": "sls-id-v1:0123...cdef", // The recovery code, or the 64-character hex key on its own
"useRemoteTweaks": true
}With a key configured, the bridge uses ID derivation version 1 unless idDerivationVersion says otherwise. Keep the key private, like the passphrase.
Synchronising internal/hidden files
This bidirectional routing change remains unreleased.
Hidden-file synchronisation is optional and disabled by default. For each
CouchDB peer, includeInternal enables synchronisation only for matching hidden
files, in both directions. Omit the field or use [] to keep it disabled.
Self-hosted LiveSync stores internal documents with an i: prefix. The bridge
supports hidden files whose first Vault-root path component begins with a dot,
such as .claude/settings.json or .settings.json.
Set includeInternal to an array of
minimatch glob patterns matched against the
complete Vault-root path after removing the i: prefix. The following example
enables synchronisation for .claude/ only:
{
"type": "couchdb",
"name": "test1",
// ...
"baseDir": "blog/",
"includeInternal": [".claude/**"] // Share selected .claude files in both directions
}A hidden file is included when its path matches any pattern. Patterns use the
dot option, so hidden subdirectories are matched too. A broad pattern such as
** still leaves ordinary notes on their ordinary route. Other internal paths,
such as i:note.md, are outside this feature.
Internal paths are relative to the Vault root, independently of baseDir.
The example selects ordinary documents below blog/ and internal files below
.claude/ at the Vault root. The internal document
i:.claude/settings.json maps to .claude/settings.json below the storage
peer's root. Local creation, edits, and deletion use that same internal document
on the return route, including after restart.
Each CouchDB peer applies its own includeInternal patterns in both directions.
Opting one peer in does not enable another peer. Each Obsidian client that
should synchronise these files also needs hidden-file synchronisation enabled
in its own LiveSync settings. An excluded hidden file has no ordinary fallback
route. Storage peers' sending exclusions remain separate; see
Excluding paths from a storage peer.
On CouchDB peers, shared paths beginning with a dot are reserved for this
internal route. Ordinary documents such as blog/.claude/settings.json are
excluded, and a hidden Vault-root folder cannot serve as an ordinary sharing
folder. Before using an internal path, the bridge checks for a live ordinary
document at baseDir joined with that shared path. If one exists, the bridge
reports both paths and blocks the internal operation, preserving both documents
and any existing storage file. A failed collision check also blocks the
operation. A deleted ordinary document does not block it. The bridge does not
merge, migrate, or delete conflicting documents automatically.
Changing includeInternal or resolving a collision does not replay previously
skipped changes. A later source change can trigger synchronisation. To rescan
existing files, restart with --reset; this clears the stored scan and resume
state for all peers.
Caution
This synchronises files that are normally hidden and internal. Such folders often hold tool configuration that can contain machine-specific paths, local settings, or secrets. Only include patterns you genuinely intend to share, and review what they match before enabling. The option is opt-in: leave it out to keep the default behaviour, where all internal/hidden files are skipped.
By default a storage peer synchronises every file below its baseDir. The
optional ignore field on a storage peer excludes paths from that. It takes an
array of minimatch glob patterns which
are matched against the path relative to baseDir:
{
"type": "storage",
"name": "storage-test1",
"baseDir": "./vault/",
"ignore": ["**/.git/**", "node_modules/**", "**/*.tmp"]
}A path is excluded when it, or any directory containing it, matches any one of
the patterns. Patterns are matched with the dot option, so leading-dot folders
such as .git/ are matched as expected. A pattern ending in /** also covers
the directory itself, so **/.git/** excludes .git as well as everything
inside it. Likewise, a pattern which matches a directory's name excludes
everything below that directory: private excludes private/note.md, and
**/*.tmp excludes the contents of a directory named cache.tmp.
Excluded directories are pruned while scanning: they are not descended into, and
nothing inside them is inspected or read. This matters for scanOfflineChanges,
because a repository such as .git/ can hold tens of thousands of files which
would otherwise all be examined at every start. Excluded paths are also skipped
for live additions, changes and deletions.
Note that ignore only stops a storage peer from sending its own changes.
Incoming changes from other peers are still accepted: a file which another peer
sends is still written, and a deletion which another peer sends is still
applied, even at an excluded path.
| name | database_uri / path | CouchDB username | CouchDB password | vault E2EE passphrase | baseDir |
|---|---|---|---|---|---|
| private vault of Cornbread | http://localhost:5984/classroom_cornbread | cornbread | tackle | glucose | shared/ |
| shared vault | http://localhost:5984/classroom_shared | common_user | resu_nommoc | cocoa | |
| private vault of Vanilla | http://localhost:5984/classroom_vanilla | vanilla | liberty | smock | kyouyuu/ |
| storage | ./vault/ |
Cornbread's every document under "shared" is synchronized with the top of the shared vault:
| Cornbread | shared |
|---|---|
| document1 | Not transferred |
| document2 | Not transferred |
| shared/shared_doc1 | shared_doc1 |
| shared/sub/sub_doc | sub/sub_doc |
Vanilla's every document under "kyouyuu" is synchronized with the top of the shared vault:
| Vanilla | shared |
|---|---|
| documentA | Not transferred |
| documentB | Not transferred |
| kyouyuu/some_doc | some_doc |
| kyouyuu/sub/some_sub_doc | sub/some_sub_doc |
Totally, all files are synchronized like this:
| Cornbread | shared | Vanilla |
|---|---|---|
| document1 | Not transferred | |
| document2 | Not transferred | |
| Not transferred | documentA | |
| Not transferred | documentB | |
| shared/shared_doc1 | shared_doc1 | kyouyuu/shared_doc1 |
| shared/some_doc | some_doc | kyouyuu/some_doc |
| shared/sub/some_sub_doc | sub/some_sub_doc | kyouyuu/sub/some_sub_doc |
| shared/sub/sub_doc | sub/sub_doc | kyouyuu/sub/sub_doc |
... with the configuration below:
{
"peers": [
{
"type": "couchdb", // Type should be `couchdb or storage`
"name": "cornbread", // Should be unique
"url": "http://localhost:5984",
"database": "classroom_cornbread",
"username": "cornbread",
"password": "tackle",
"passphrase": "glucose", // E2EE passphrase, if you do not enabled, leave it blank.
"obfuscatePassphrase": "glucose", // Path obfuscation passphrase. Leave blank to disable it; this may differ from passphrase.
"customChunkSize": 100,
"minimumChunkSize": 20,
"baseDir": "shared/" // Sharing folder
},
{
"type": "couchdb", // Type should be `couchdb or storage`
"name": "shared", // Should be unique
"url": "http://localhost:5984",
"database": "classroom_shared",
"username": "common_user",
"password": "resu_nommoc",
"passphrase": "cocoa", // E2EE passphrase, if you do not enabled, leave it blank.
"obfuscatePassphrase": "cocoa", // Path obfuscation passphrase. Leave blank to disable it; this may differ from passphrase.
"customChunkSize": 100,
"minimumChunkSize": 20,
"baseDir": "" // Sharing folder
},
{
"type": "couchdb",
"name": "vanilla", // We can even synchronise the same databases as long as they have different names in here.
"url": "http://localhost:5984",
"database": "classroom_vanilla",
"username": "vanilla",
"password": "liberty",
"passphrase": "smock",
"obfuscatePassphrase": "smock",
"customChunkSize": 100,
"minimumChunkSize": 20,
"baseDir": "kyouyuu/"
},
{
"type": "storage",
"name": "storage-test1",
"baseDir": "./vault/" // The folder which have been synchronised.
}
]
}- Internal-file synchronisation design: accepted path, selection, and verification contract; implementation remains unreleased.
- Internal-file synchronisation ADR: rationale, alternatives, and compatibility effects.

{ "disableCheckingConfigMismatch": true, "peers": [ /* Existing peer configuration */ ] }