Private Sync Server is a self-hosted backend for synchronizing Obsidian vaults with the Private Sync plugin.
Obsidian plugin: https://github.com/Haniewicz/PrivateSyncPlugin
The server stores the logical state of vaults in SQLite and keeps file contents as blobs on disk. It does not keep a one-to-one browsable Obsidian vault directory. The local vault is reconstructed and updated by the plugin from revisions fetched through the API.
- A Linux VPS or another host with Node.js.
- Node.js 22 LTS or newer.
npm.git.- An HTTPS reverse proxy, such as Caddy or Nginx, if the server should be reachable from the internet.
- A persistent data directory, for example
/var/lib/private-sync-server.
git clone https://github.com/Haniewicz/PrivateSyncServer.git
cd PrivateSyncServer
npm install
npm run syncctl -- setup --password "change-this-password"
npm run devBy default, the server listens on http://127.0.0.1:8787 and stores data in data/server.sqlite and data/blobs.
The example below installs the server in /opt/private-sync-server, stores data in /var/lib/private-sync-server, and runs the process through systemd.
Debian/Ubuntu:
sudo apt update
sudo apt install -y git curl ca-certificates build-essentialInstall Node.js 22 LTS. Example using NodeSource:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node --version
npm --versionsudo useradd --system --home /opt/private-sync-server --shell /usr/sbin/nologin private-sync
sudo mkdir -p /opt/private-sync-server
sudo mkdir -p /var/lib/private-sync-server
sudo chown -R private-sync:private-sync /opt/private-sync-server /var/lib/private-sync-serversudo -u private-sync git clone https://github.com/Haniewicz/PrivateSyncServer.git /opt/private-sync-server
cd /opt/private-sync-server
sudo -u private-sync npm ci
sudo -u private-sync npm run buildThe password must be at least 8 characters long.
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- setup --password "use-a-strong-password"
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- config showsetup creates the SQLite database, sets the server password, and enables initial setup for the first trusted device. The first device can pair without approval from another device. Additional devices require approval or a recovery pairing code.
sudo tee /etc/private-sync-server.env >/dev/null <<'EOF'
NODE_ENV=production
HOST=127.0.0.1
PORT=8787
PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server
TRUST_PROXY=true
AUTH_RATE_LIMIT_MAX=10
AUTH_RATE_LIMIT_WINDOW_SECONDS=60
PAIRING_STATUS_RATE_LIMIT_MAX=30
PAIRING_STATUS_RATE_LIMIT_WINDOW_SECONDS=60
EOF
sudo chmod 600 /etc/private-sync-server.envImportant variables:
HOST- application listen address, default127.0.0.1.PORT- application port, default8787.PRIVATE_SYNC_DATA_DIR- data directory, default./data.DATABASE_PATH- optional SQLite path, default$PRIVATE_SYNC_DATA_DIR/server.sqlite.BLOB_DIR- optional blob directory, default$PRIVATE_SYNC_DATA_DIR/blobs.TRUST_PROXY- set totruewhen the server runs behind a reverse proxy.
sudo tee /etc/systemd/system/private-sync-server.service >/dev/null <<'EOF'
[Unit]
Description=Private Sync Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=private-sync
Group=private-sync
WorkingDirectory=/opt/private-sync-server
EnvironmentFile=/etc/private-sync-server.env
ExecStart=/usr/bin/npm run start
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/private-sync-server /opt/private-sync-server
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now private-sync-server.service
sudo systemctl status private-sync-server.serviceCaddy example:
sync.example.com {
reverse_proxy 127.0.0.1:8787
}Nginx example:
server {
listen 443 ssl http2;
server_name sync.example.com;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}WebSocket runs under /api/v1/events?token=DEVICE_TOKEN, so the reverse proxy must support HTTP upgrade.
curl https://sync.example.com/api/v1/server-info
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password http-verify --url "https://sync.example.com"In the Obsidian plugin, set Server URL to https://sync.example.com.
cd /opt/private-sync-server
sudo -u private-sync git pull --ff-only origin master
sudo -u private-sync npm ci
sudo -u private-sync npm run build
sudo systemctl restart private-sync-server.service
sudo systemctl status private-sync-server.service
curl https://sync.example.com/api/v1/server-infoSQLite migrations run automatically when the process starts.
Backups must include all of the following at the same time:
- the SQLite database, default
/var/lib/private-sync-server/server.sqlite, - the blob directory, default
/var/lib/private-sync-server/blobs, - the staging directory, if you want to preserve unfinished uploads, default
/var/lib/private-sync-server/staging.
The simplest backup with the service stopped:
sudo systemctl stop private-sync-server.service
sudo tar -czf private-sync-backup-$(date +%F).tar.gz -C /var/lib private-sync-server
sudo systemctl start private-sync-server.serviceFor online backups, use a tool that supports consistent filesystem snapshots or the SQLite backup API. Do not copy the blobs directory alone without the matching SQLite database.
Run commands with the same PRIVATE_SYNC_DATA_DIR used by systemd:
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- config show
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password verify
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password reset
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password reset --password "new-password"
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password http-verify --url "https://sync.example.com"
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- pairing-code create --ttl=10m
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- initial-setup enable
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- initial-setup disablepairing-code create creates a one-time recovery pairing code. This code allows a new device to pair without approval from another device, but the plugin still requires the server password.
password reset changes the main server login password. It does not revoke existing device_token values and does not recover or change data encryption keys.
Key endpoints:
GET /api/v1/server-infoPOST /api/v1/auth/loginPOST /api/v1/devices/requestPOST /api/v1/devices/approvePOST /api/v1/devices/revokePOST /api/v1/devices/restorePOST /api/v1/devices/deleteGET /api/v1/devicesGET /api/v1/vaultsPOST /api/v1/vaultsPOST /api/v1/vaults/:vaultId/renamePOST /api/v1/vaults/:vaultId/deleteGET /api/v1/vaults/:vaultId/community-pluginsPUT /api/v1/vaults/:vaultId/community-pluginsPOST /api/v1/vaults/:vaultId/connection-assessmentPOST /api/v1/vaults/:vaultId/sync-stateGET /api/v1/vaults/:vaultId/changes?since=0POST /api/v1/vaults/:vaultId/sync-batchesPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/uploadPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/chunked-uploadPUT /api/v1/vaults/:vaultId/sync-batches/:batchId/chunked-upload/:uploadId/chunks/:chunkIndexPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/chunked-upload/:uploadId/finishPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/commitGET /api/v1/vaults/:vaultId/files/download?path=note.mdGET /api/v1/vaults/:vaultId/files/history?path=note.mdGET /api/v1/vaults/:vaultId/requestsPOST /api/v1/vaults/:vaultId/requests/:requestId/resolve
Server features:
- multiple server vaults,
- device tokens,
- recovery pairing codes,
- batch upload and commit,
- chunked upload/download for large files,
- global vault revisions,
- file history,
- conflicts and decision requests,
- safety assessment when connecting a local vault to a server vault,
- community plugin catalog and JSON settings files,
- metadata for client-side encryption and key rotation.
The server runs as a single Node.js/Fastify application.
- Metadata is stored in SQLite.
- File contents are stored in a blob directory by SHA-256.
- WebSocket does not transfer files. It is only used for events such as
vault_changed,request_created, andconflict_created. - Actual synchronization operations happen through the HTTP API.
- Changes are uploaded in batches:
- the plugin creates a batch with the operation list,
- uploads changed file contents to the staging area,
- asks the server to commit the batch,
- the server validates the batch and publishes a new global vault revision.
- If a batch is interrupted halfway through, unfinished staging files do not become the current vault state.
- The server detects conflicts by comparing
base_revision_idwith the current file revision on the server. - The server detects potentially dangerous operations, such as mass deletion, and pauses the batch for a user decision.
For one person and a few devices, this is usually enough:
- 1 vCPU,
- 1-2 GB RAM,
- 20-40 GB SSD plus space for history and backups,
- regular backups of the data directory.
For several people or a large vault with attachments, start with:
- 2 vCPU,
- 2-4 GB RAM,
- 80+ GB SSD/NVMe,
- an upload limit adjusted to the largest files,
- automated backups of SQLite and blob storage.
For heavier use, consider PostgreSQL, object storage for blobs, a worker queue, per-user quotas, metrics, and alerts.
systemd logs:
sudo journalctl -u private-sync-server.service -fIf the plugin returns invalid_password, check whether the CLI and public URL use the same database:
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- config show
curl https://sync.example.com/api/v1/server-info
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password http-verify --url "https://sync.example.com"Compare instanceId from config show and /server-info. If they differ, the CLI and public URL are pointing at different instances or different databases.
After password reset, a server restart is not required because the password hash is read from the database on every login.
Polski
Private Sync Server to prywatny backend synchronizacji vaultow Obsidiana dla pluginu Private Sync.
Plugin Obsidiana: https://github.com/Haniewicz/PrivateSyncPlugin
Serwer przechowuje logiczny stan vaultow w SQLite oraz tresc plikow jako bloby na dysku. Nie przechowuje gotowego katalogu vaulta Obsidiana jeden do jednego. Lokalny vault jest odtwarzany i aktualizowany przez plugin na podstawie rewizji pobieranych z API.
- Linux VPS albo inny host z Node.js.
- Node.js 22 LTS lub nowszy.
npm.git.- Reverse proxy z HTTPS, np. Caddy albo Nginx, jesli serwer ma byc dostepny z internetu.
- Staly katalog danych, np.
/var/lib/private-sync-server.
git clone https://github.com/Haniewicz/PrivateSyncServer.git
cd PrivateSyncServer
npm install
npm run syncctl -- setup --password "zmien-to-haslo"
npm run devDomyslnie serwer slucha na http://127.0.0.1:8787, a dane trzyma w data/server.sqlite i data/blobs.
Ponizszy przyklad instaluje serwer w /opt/private-sync-server, dane trzyma w /var/lib/private-sync-server, a proces uruchamia przez systemd.
Debian/Ubuntu:
sudo apt update
sudo apt install -y git curl ca-certificates build-essentialZainstaluj Node.js 22 LTS. Przyklad przez NodeSource:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node --version
npm --versionsudo useradd --system --home /opt/private-sync-server --shell /usr/sbin/nologin private-sync
sudo mkdir -p /opt/private-sync-server
sudo mkdir -p /var/lib/private-sync-server
sudo chown -R private-sync:private-sync /opt/private-sync-server /var/lib/private-sync-serversudo -u private-sync git clone https://github.com/Haniewicz/PrivateSyncServer.git /opt/private-sync-server
cd /opt/private-sync-server
sudo -u private-sync npm ci
sudo -u private-sync npm run buildHaslo musi miec co najmniej 8 znakow.
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- setup --password "wstaw-mocne-haslo"
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- config showsetup tworzy baze SQLite, ustawia haslo serwera i wlacza initial setup dla pierwszego zaufanego urzadzenia. Pierwsze urzadzenie moze sparowac sie bez akceptacji z innego urzadzenia. Kolejne urzadzenia wymagaja akceptacji albo recovery pairing code.
sudo tee /etc/private-sync-server.env >/dev/null <<'EOF'
NODE_ENV=production
HOST=127.0.0.1
PORT=8787
PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server
TRUST_PROXY=true
AUTH_RATE_LIMIT_MAX=10
AUTH_RATE_LIMIT_WINDOW_SECONDS=60
PAIRING_STATUS_RATE_LIMIT_MAX=30
PAIRING_STATUS_RATE_LIMIT_WINDOW_SECONDS=60
EOF
sudo chmod 600 /etc/private-sync-server.envNajwazniejsze zmienne:
HOST- adres nasluchiwania aplikacji, domyslnie127.0.0.1.PORT- port aplikacji, domyslnie8787.PRIVATE_SYNC_DATA_DIR- katalog danych, domyslnie./data.DATABASE_PATH- opcjonalna sciezka do SQLite, domyslnie$PRIVATE_SYNC_DATA_DIR/server.sqlite.BLOB_DIR- opcjonalny katalog blobow, domyslnie$PRIVATE_SYNC_DATA_DIR/blobs.TRUST_PROXY- ustawtrue, gdy serwer stoi za reverse proxy.
sudo tee /etc/systemd/system/private-sync-server.service >/dev/null <<'EOF'
[Unit]
Description=Private Sync Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=private-sync
Group=private-sync
WorkingDirectory=/opt/private-sync-server
EnvironmentFile=/etc/private-sync-server.env
ExecStart=/usr/bin/npm run start
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/private-sync-server /opt/private-sync-server
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now private-sync-server.service
sudo systemctl status private-sync-server.servicePrzyklad Caddy:
sync.example.com {
reverse_proxy 127.0.0.1:8787
}Przyklad Nginx:
server {
listen 443 ssl http2;
server_name sync.example.com;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}WebSocket dziala pod /api/v1/events?token=DEVICE_TOKEN, dlatego reverse proxy musi obslugiwac upgrade HTTP.
curl https://sync.example.com/api/v1/server-info
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password http-verify --url "https://sync.example.com"W pluginie Obsidiana wpisz Server URL jako https://sync.example.com.
cd /opt/private-sync-server
sudo -u private-sync git pull --ff-only origin master
sudo -u private-sync npm ci
sudo -u private-sync npm run build
sudo systemctl restart private-sync-server.service
sudo systemctl status private-sync-server.service
curl https://sync.example.com/api/v1/server-infoMigracje SQLite sa wykonywane automatycznie przy starcie procesu.
Backup musi obejmowac jednoczesnie:
- baze SQLite, domyslnie
/var/lib/private-sync-server/server.sqlite, - katalog blobow, domyslnie
/var/lib/private-sync-server/blobs, - katalog staging, jesli chcesz zachowac niedokonczone uploady, domyslnie
/var/lib/private-sync-server/staging.
Najprostszy backup przy zatrzymanej usludze:
sudo systemctl stop private-sync-server.service
sudo tar -czf private-sync-backup-$(date +%F).tar.gz -C /var/lib private-sync-server
sudo systemctl start private-sync-server.servicePrzy backupie online uzyj narzedzia obslugujacego spojny snapshot filesystemu albo SQLite backup API. Nie kopiuj samego katalogu blobs bez odpowiadajacej mu bazy SQLite.
Uruchamiaj komendy z tym samym PRIVATE_SYNC_DATA_DIR, ktorego uzywa systemd:
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- config show
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password verify
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password reset
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password reset --password "nowe-haslo"
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password http-verify --url "https://sync.example.com"
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- pairing-code create --ttl=10m
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- initial-setup enable
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- initial-setup disablepairing-code create tworzy jednorazowy recovery pairing code. Taki kod pozwala sparowac nowe urzadzenie bez akceptacji na innym urzadzeniu, ale nadal wymaga hasla serwera w pluginie.
password reset zmienia glowne haslo logowania do serwera. Nie uniewaznia istniejacych device_token i nie odzyskuje ani nie zmienia kluczy szyfrowania danych.
Najwazniejsze endpointy:
GET /api/v1/server-infoPOST /api/v1/auth/loginPOST /api/v1/devices/requestPOST /api/v1/devices/approvePOST /api/v1/devices/revokePOST /api/v1/devices/restorePOST /api/v1/devices/deleteGET /api/v1/devicesGET /api/v1/vaultsPOST /api/v1/vaultsPOST /api/v1/vaults/:vaultId/renamePOST /api/v1/vaults/:vaultId/deleteGET /api/v1/vaults/:vaultId/community-pluginsPUT /api/v1/vaults/:vaultId/community-pluginsPOST /api/v1/vaults/:vaultId/connection-assessmentPOST /api/v1/vaults/:vaultId/sync-stateGET /api/v1/vaults/:vaultId/changes?since=0POST /api/v1/vaults/:vaultId/sync-batchesPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/uploadPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/chunked-uploadPUT /api/v1/vaults/:vaultId/sync-batches/:batchId/chunked-upload/:uploadId/chunks/:chunkIndexPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/chunked-upload/:uploadId/finishPOST /api/v1/vaults/:vaultId/sync-batches/:batchId/commitGET /api/v1/vaults/:vaultId/files/download?path=note.mdGET /api/v1/vaults/:vaultId/files/history?path=note.mdGET /api/v1/vaults/:vaultId/requestsPOST /api/v1/vaults/:vaultId/requests/:requestId/resolve
Funkcje serwera:
- wiele server-vaultow,
- device tokens,
- recovery pairing code,
- batch upload i commit,
- chunked upload/download duzych plikow,
- globalne rewizje vaulta,
- historia plikow,
- konflikty i requesty decyzyjne,
- ocena bezpieczenstwa laczenia lokalnego vaulta z server-vaultem,
- katalog community pluginow i JSON-owych plikow ustawien,
- metadane klientowego szyfrowania i rotacji kluczy.
Serwer dziala jako pojedyncza aplikacja Node.js/Fastify.
- Baza metadanych to SQLite.
- Tresc plikow jest trzymana w katalogu blobow po SHA-256.
- WebSocket nie przesyla plikow. Sluzy tylko do eventow, np.
vault_changed,request_created,conflict_created. - Realne operacje synchronizacji ida przez HTTP API.
- Upload zmian odbywa sie batchami:
- plugin tworzy batch z lista operacji,
- wysyla tresci zmienionych plikow do staging area,
- prosi serwer o commit batcha,
- serwer waliduje batch i publikuje nowa globalna rewizje vaulta.
- Jesli batch jest przerwany w polowie, niedokonczone pliki stagingowe nie staja sie aktualnym stanem vaulta.
- Serwer wykrywa konflikty przez porownanie
base_revision_idz aktualna rewizja pliku na serwerze. - Serwer wykrywa potencjalnie niebezpieczne operacje, np. masowe usuwanie, i zatrzymuje batch do decyzji uzytkownika.
Dla jednej osoby i kilku urzadzen zwykle wystarczy:
- 1 vCPU,
- 1-2 GB RAM,
- 20-40 GB SSD plus miejsce na historie i backupy,
- regularny backup katalogu danych.
Dla kilku osob albo duzego vaulta z zalacznikami lepiej zaczac od:
- 2 vCPU,
- 2-4 GB RAM,
- 80+ GB SSD/NVMe,
- limit uploadu dopasowany do najwiekszych plikow,
- automatyczne backupy SQLite i blob storage.
Przy wiekszym uzyciu warto rozwazyc PostgreSQL, object storage dla blobow, kolejke workerow, quota per uzytkownik, metryki i alerty.
Logi systemd:
sudo journalctl -u private-sync-server.service -fJesli plugin zwraca invalid_password, sprawdz czy CLI i publiczny URL korzystaja z tej samej bazy:
cd /opt/private-sync-server
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- config show
curl https://sync.example.com/api/v1/server-info
sudo -u private-sync PRIVATE_SYNC_DATA_DIR=/var/lib/private-sync-server npm run syncctl -- password http-verify --url "https://sync.example.com"Porownaj instanceId z config show i /server-info. Jesli jest rozny, CLI i publiczny URL trafiaja w inne instancje albo inne bazy.
Po password reset restart serwera nie jest wymagany, bo hash hasla jest czytany z bazy przy kazdym logowaniu.