Sync notes, files, settings and themes across devices in real time, end-to-end encrypted, via your own self-hosted server.
End-to-end encrypted, real-time sync for your Obsidian vault, backed by a self-hosted Go/PostgreSQL server you control. Notes, plugin settings, themes and binaries stay in sync across devices using a CRDT engine (Loro) for conflict-free merging, while a client-side AES-256-GCM key keeps the server unable to read your content.
This is not a hosted service. Ilow Sync has no managed backend run by the developer. You must deploy your own server (a single script sets up PostgreSQL + the Go binary + systemd on a Linux host) before the plugin can sync anything. See Self-hosting the backend below.
In line with Obsidian's developer policies, here is exactly what this plugin does and doesn't do:
backend/, which you deploy yourself — see below). It is used to store encrypted snapshots/updates and to relay real-time change notifications over WebSocket. No other network endpoint is contacted, and no data is ever sent to the plugin author or any third-party service./api/telemetry endpoint (uptime, memory, request rate) that the plugin's sidebar reads to show you your own server's health. This data never leaves your infrastructure and is not collected by the developer..obsidian/ plugin/theme/config files) live inside your vault.Ilow Sync is not yet in the Community Plugins directory. Until then, install it manually or via BRAT:
main.js, manifest.json and styles.css from the latest release.ilow-sync inside your vault's .obsidian/plugins/ directory and place the three files there.The plugin needs a running instance of the Go/PostgreSQL backend to sync against. On a fresh Debian/Ubuntu server (or VM):
curl -fsSL https://raw.githubusercontent.com/Ilow-Space/IlowObsidianSync/main/setup_back.sh -o setup_back.sh
sudo bash setup_back.sh
This installs PostgreSQL, provisions a database, downloads (or compiles) the ilow-backend binary, generates your Access API Key and Admin API Key, and registers a systemd service so the server survives reboots. If Nginx is already running on the box, the script can also wire up TLS termination and API-key checks at the proxy level.
Prefer to run it manually? The backend only needs:
| Variable | Example | Description |
|---|---|---|
PORT |
3001 |
HTTP & WebSocket port |
DATABASE_URL |
postgres://user:pass@localhost:5432/ilow_db?sslmode=disable |
PostgreSQL connection string |
API_KEY |
(random) | Required by clients for REST/WebSocket auth |
ADMIN_API_KEY |
(random) | Required for /api/admin/truncate maintenance calls |
Then run go build ./backend and start the resulting binary with those variables in its environment (or a .env file next to it). The schema (vault_snapshots, vault_updates) is created automatically on first start.
Open Settings → Ilow Sync in Obsidian:
| Setting | Description |
|---|---|
| Base URL | Your backend's HTTPS endpoint, e.g. https://sync.example.com |
| API Key | The access key generated by setup_back.sh (or your own API_KEY) |
| Admin API Token | Optional — only needed to use the "Purge Server Data" maintenance action |
| Cryptography Salt | Auto-generated on first run, or imported via QR code from another device |
| Master Password | Used locally to derive your AES-256 key. Never transmitted or written to disk. |
Click Derive Key after entering your Master Password to activate sync. Use Test Connection to confirm the plugin can reach your server before relying on it.
Instead of copying the Base URL, API Key and Salt by hand:
ACCESS_API_KEY.ACCESS_API_KEY — the backend now authenticates every route itself rather than relying on an optional Nginx rule. Put the Access API Key from setup_back.sh in ACCESS_API_KEY in backend/.env, or set ALLOW_UNAUTHENTICATED=true if something in front of it already authenticates. Servers set up before this change have the key under API_KEY only; copy it across.The codebase follows Domain-Driven Design across the client and an independent backend:
src/1_Domain — entities (Note, CRDTSnapshot, CRDTUpdate, VFSNode) and contracts (ICryptography, IRemoteStore, INoteRepository).src/2_Application — NetworkOrchestrator (polling, push queues, WebSocket events, full-vault hydration), LoroVfsController (tree operations, UUID↔path mapping), ObsidianDiskReconciler (CRDT graph → vault file operations), SyncEventBus (typed events via mitt + zod), VaultEventWatcher (native vault lifecycle hooks).src/3_Infrastructure — LoroSyncEngine (Loro document lifetimes, state vectors, diffing), LoroSnapshotStore (Dexie/IndexedDB offline cache), WebCryptoService (PBKDF2 + AES-GCM), PostgresRemoteStore (REST/WebSocket transport over Obsidian's requestUrl).src/4_Presentation — Plugin.ts (entrypoint, secret storage, lifecycle), SettingsTab.ts, SyncSidebarView.ts (live status, throughput, queue depth), QR modals for onboarding.backend/ — Go server providing the REST API, PostgreSQL LISTEN/NOTIFY → WebSocket bridge, and idle-time maintenance (VACUUM ANALYZE, memory release).| Method | Endpoint | Function |
|---|---|---|
GET |
/ |
Upgrade to WebSocket for realtime notifications |
GET |
/api/telemetry |
Server health, memory, uptime, RPS, DB connection stats |
GET |
/api/vault/manifest |
List of document snapshots and encrypted path identifiers |
GET |
/api/vault/latest_ids |
Bulk fetch of the highest update ID per document |
GET |
/api/snapshots/{id} |
Snapshot metadata and state payload for one document |
GET |
/api/snapshots/{id}/updates?since={N} |
Incremental update deltas after update ID N |
GET |
/api/snapshots/{id}/latest_id |
Latest update sequence ID for one document |
POST |
/api/updates |
Push an encrypted incremental delta |
POST |
/api/snapshots/{id}/compact |
Overwrite base snapshot, truncate merged history |
DELETE |
/api/snapshots/{id} |
Soft-delete a snapshot and its updates |
POST |
/api/admin/truncate |
Truncate all tables (requires Authorization: Bearer <ADMIN_API_KEY>) |
CREATE TABLE IF NOT EXISTS vault_snapshots (
document_id TEXT PRIMARY KEY,
encrypted_state BYTEA,
encrypted_path BYTEA,
is_deleted BOOLEAN DEFAULT false,
max_compacted_id INT DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS vault_updates (
id SERIAL PRIMARY KEY,
document_id TEXT NOT NULL REFERENCES vault_snapshots(document_id) ON DELETE CASCADE,
encrypted_update BYTEA,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_vault_updates_doc_id ON vault_updates(document_id);
A database trigger publishes a JSON payload on the vault_updates_channel whenever a row is inserted into vault_updates, which the Go server relays to connected WebSocket clients.
# Start development watcher (Vite CJS compilation)
npm run dev
# Production build (typecheck, bundle, copy manifest to dist/)
npm run build
# Typecheck TypeScript without emitting JS
npm run typecheck
# Run unit & integration tests via Vitest
npm run test:unit
# Run full WebdriverIO multi-vault E2E tests in real Obsidian instances
npm run test:e2e
# Code duplication checks (jscpd) and lint rules (ESLint)
npm run compliance
# Everything: typecheck, lint, dry, unit, e2e
npm run test:all
The backend is a standard Go module:
cd backend
go build -o ilow-backend
go test ./...
Ilow Sync (both the Obsidian plugin and the backend/ server) is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0) — see LICENSE for the full text.
The AGPL was chosen deliberately because this project ships a server component: if you modify the backend and make it available to others over a network (e.g. run a hosted version of it), you are required to also make your modified source available to those users under the same license.