Small sidecar that watches your Syncthing instance and sends a wake-up signal to the VaultSync Cloud Relay when your server changes. The relay forwards a silent APNs push, so VaultSync syncs promptly instead of waiting for the next background refresh. No file content or metadata leaves your server — only a Device ID.
Cloud Relay accelerates server → iPhone only. For iPhone → server, open VaultSync (see Product scope below).
On the machine that runs Syncthing (server, NAS, or an always-on computer), run this one line — it is the entire setup:
curl -fsSL https://vaultsync.eu/notify.sh | shNothing to edit, no API key to copy.
The installer (scripts/install.sh) finds your config.xml, runs the helper as the uid:gid that owns it (the #1 setup failure), and starts it — as a Docker container when Docker is available, otherwise as a prebuilt binary behind a systemd service (Linux) or launchd agent (macOS). It ends with the helper's own --doctor preflight, so a misconfiguration fails loudly with the fix spelled out.
The moment the helper starts it sends one wake-up, and VaultSync flips to Cloud Relay active on its own.
- Skeptical of
curl | sh? Append-s -- --dry-runto preview every action without changing anything, or read the script first. - Config in a non-standard place?
curl -fsSL https://vaultsync.eu/notify.sh | SYNCTHING_CONFIG=/path/to/config.xml sh— the variable must prefixsh(the installer), notcurl. Synology/QNAP/Unraid host layouts are probed automatically. - The script contains nothing user-specific — identity comes from your own Syncthing's Device ID at runtime.
Windows — one step too, in PowerShell (scripts/install.ps1):
irm https://vaultsync.eu/notify.ps1 | iexIt finds config.xml under %LOCALAPPDATA%\Syncthing, downloads the prebuilt helper with SHA-256 verification, registers a per-user Scheduled Task (runs hidden at logon, restarts on failure — no admin rights, no service wrapper), starts it, and ends with --doctor. Skeptical of irm | iex? Set $env:VAULTSYNC_NOTIFY_DRYRUN = '1' first to preview every action, or read the script. Config elsewhere? Set $env:SYNCTHING_CONFIG before running. Re-running upgrades the helper. Logs land in %LOCALAPPDATA%\VaultSync\vaultsync-notify.log.
The one line above is all most setups need. Prefer to run things yourself? Every path below is equivalent — pick one and expand it.
🐳 Docker Compose — Syncthing and the helper together, key-free
Runs Syncthing and the helper together. The helper reads the Syncthing API key from the shared config.xml, so there's no key to copy — the only value you supply is RELAY_URL.
cd notify
cp .env.example .env # RELAY_URL defaults to the production relay
docker compose up -dA plain
docker compose upsends one real wake-up to production — intended for subscribers. Testing locally? OverrideRELAY_URLto a mock first (see the header ofdocker-compose.yml).
🖥️ Run next to a host Syncthing — paste-and-go docker run or guided bootstrap.sh
Syncthing running natively on the host (not in Compose)? Use either path — both auto-detect the key from config.xml.
A. Paste-and-go docker run (the manual alternative VaultSync shows after you subscribe):
docker run -d --name vaultsync-notify --restart unless-stopped \
--network host \
-v /PATH/TO/syncthing:/config:ro \
-e SYNCTHING_CONFIG=/config/config.xml \
-e RELAY_URL=https://relay.vaultsync.eu \
ghcr.io/psimaker/vaultsync-notify:2.0.2Replace /PATH/TO/syncthing with your Syncthing config folder (often ~/.local/state/syncthing or ~/.config/syncthing). Permission error? Add -u <uid>:<gid> for the user that owns config.xml.
B. Guided bootstrap.sh — detects config.xml, validates Syncthing + relay connectivity, writes a Compose-safe notify/.env, and runs --doctor. It does not start the Compose stack.
cd notify && ./scripts/bootstrap.shIf detection fails, set SYNCTHING_CONFIG=/path/to/config.xml and rerun.
📦 Prebuilt binaries — Linux, macOS, Windows; no Docker, no Go toolchain
Every notify-v* release ships static binaries for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, and windows/amd64 — no Docker, no Go toolchain. The one-step installer downloads and verifies these automatically on Linux (systemd service) and macOS (launchd agent); grab them manually from the releases page for anything else, and check the download against the release's SHA256SUMS. A missing checksum asset or local SHA-256 implementation aborts installation rather than accepting an unverified binary.
Run manually — the only required value is RELAY_URL; the Syncthing key and URL are auto-detected from config.xml:
RELAY_URL=https://relay.vaultsync.eu ./vaultsync-notify⚙️ Environment variables — full reference, NAS permission notes
| Variable | Required | Default | Description |
|---|---|---|---|
RELAY_URL |
Yes | — (binary) | Relay endpoint. No built-in default on purpose, so the helper never wakes a relay you didn't choose. docker-compose.yml and the in-app command set it to https://relay.vaultsync.eu. |
SYNCTHING_API_KEY |
No | auto-detected | Read from config.xml (<gui><apikey>) when unset — no need to copy it from the Web UI. Set to override. |
SYNCTHING_API_URL |
No | auto / http://localhost:8384 |
Read from config.xml (<gui><address>) when unset. Set when Syncthing is a sibling container (e.g. http://syncthing:8384). |
SYNCTHING_CONFIG |
No | standard locations | Explicit path to config.xml. When unset, standard per-platform, container and NAS host paths are probed (incl. /var/syncthing/config/config.xml, /config/config.xml, /var/packages/syncthing/… on Synology, /share/*/.qpkg/… on QNAP, /mnt/user/appdata/syncthing/… on Unraid). |
STARTUP_ANNOUNCE |
No | true |
Send one wake-up on startup so the app self-activates. Set false to suppress it — change-driven delivery still works. |
SYNCTHING_CONFIG_WAIT_SECONDS |
No | 60 |
First boot: wait up to this many seconds for Syncthing to write config.xml before exiting. 0 disables the wait (fail fast). |
DEBOUNCE_SECONDS |
No | 5 |
Wait after the last event before triggering. Batches rapid changes into one push. |
WATCHED_FOLDERS |
No | all | Comma-separated Syncthing folder IDs to watch. Empty = all. |
STALE_RETRIGGER_SECONDS |
No | 21600 (6 h) |
While a peer still needs data, re-send a wake-up on this cadence. Recovers phones that missed a push (APNs silent pushes expire after ~1 h) without waiting for the next vault change. 0 disables. |
VAULTSYNC_DIAGNOSTICS_CONFIG |
No | — | Absolute read-only runtime JSON. Diagnostics starts only when this and VAULTSYNC_DIAGNOSTICS_STATE are both present; use only the supported explicit Docker script. |
VAULTSYNC_DIAGNOSTICS_STATE |
No | — | Absolute separate mode-0700 writable state directory. Supplying only one diagnostics value is a fatal configuration error. |
NAS users (the common footgun).
config.xmlis mode0600, so the helper must run as the uid that owns it or it can't read the key (you'll get a clear permission error). The1000default fits the officialsyncthing/syncthingimage; setPUID/PGIDfor others — linuxserver =911, Unraid =99:100, Synology runs as thesyncthinguser. Synology/QNAP/Unraid host paths are probed automatically; setSYNCTHING_CONFIGonly when yourconfig.xmllives somewhere unusual.
First boot is briefly noisy — that's expected. On a fresh docker compose up, the helper can start before Syncthing has written config.xml. It waits up to SYNCTHING_CONFIG_WAIT_SECONDS, then (if still missing) exits and restart: unless-stopped retries until the file exists — a few noisy seconds, then it settles. The helper also needs Syncthing running: docker compose up vaultsync-notify alone (empty volume, no Syncthing) finds no config.
The source tree now connects the reviewed D022–D024 foundations to a local
helper runtime, but only behind two explicit operator-supplied values:
VAULTSYNC_DIAGNOSTICS_CONFIG and VAULTSYNC_DIAGNOSTICS_STATE. With neither
value, the helper opens no diagnostics listener, creates no state or credential,
and changes no folder, trust, namespace, artifact, Syncthing setting, or Relay
request. The ordinary one-line installers, PowerShell installer, bootstrap
script, and Docker Compose set neither value, so existing-user upgrades remain
Trigger-v1-only.
The configured runtime uses TLS 1.3 with a QR-delivered SPKI pin and exact
Ed25519 application signatures. Its URLs are fixed; identifiers and paths never
enter URLs or logs. Pairing, namespace creation, lifecycle rotation/revocation,
and trust all require explicit app and local-operator actions. Capability and
every operation recheck the exact local folder mode, pause state, current
expanded Syncthing ignores, pinned Device ID before and after those reads,
authenticated namespace, immutable authorization, exact ephemeral
host-path/mount-identity digest, keys, epochs, bindings, limits, and signatures.
Any Syncthing ignore-parser error fails closed.
Folder and expanded-ignore responses also have fixed byte, count, and
pattern-length ceilings.
For this supported host-bind row the Syncthing API must be an exact local
http://127.0.0.1:<port> endpoint. Syncthing, Relay, and local operator HTTP
clients reject redirects. It never
edits .stignore, shares, peers, discovery, or Syncthing configuration.
The explicitly selected diagnostics listener uses an unprivileged port from
1024 through 65535; no bind capability is added.
Every remote, operator, namespace, lifecycle, and admin mutation is serialized
through one protected cross-process lock, including commands run with
docker exec.
Only rootful Docker Engine on an explicitly confirmed standard Linux host is a
supported diagnostics package. The separate
scripts/diagnostics-docker.sh flow uses a
temporary exact parent bind solely for confirmed namespace creation, then
restarts with only the exact existing VaultSync Diagnostics child read/write,
a separate state bind, read-only runtime/config.xml files, read-only container
root, all capabilities dropped, and no-new-privileges. It resolves the helper
image to an immutable local content ID before deployment and rejects a
root-owned Syncthing config instead of running the helper as root. The installer
pins the source directory identity before the temporary bind and injects only a
SHA-256 mount-binding digest at runtime; the raw host path is not stored or
logged. Named volumes, their backing paths and subpaths, rootless Docker, remote
Docker daemons/contexts, non-Unix Docker endpoints, remote/NAS/FUSE storage,
Docker Desktop, WSL, systemd binaries, macOS, and Windows remain
diagnostics-unsupported.
The runtime rejects non-Linux activation and requires the operator config to be
owner-only mode 0400 with a single filesystem link. Before namespace creation,
the installer displays the exact resulting path and separately requires
acknowledgement of that path and possible peer/backup/version/conflict/tombstone
retention.
The installer has only forward, explicit crash completion. If namespace
creation and its protected root record became durable before the mount config
was written, rerunning diagnostics-docker.sh enable can resume only that exact
authenticated root after all Device/folder/ignore, source-inode, state, digest,
key/epoch, signature, and fixed-layout checks. Recovery mode creates nothing and
never adopts an unregistered or conflicting directory. Exact D023 authorization
messages are likewise idempotent after their file and state became durable,
even when the original signed candidate has since expired;
helper/TLS rotation remains unavailable while a registered root lacks its exact
authorization.
Each app installation pairs and authorizes independently. Another installation may join an existing authenticated namespace only through its own signed D022 and D023 flow; there is no second namespace creation or transfer of trust or identity. Revoked immutable authorization records remain historical. A later namespace-wide helper-key manifest does not rewrite them, while every active installation still needs a fresh exact D023 authorization epoch before it can resume operations. Manifest rotation also repeats the pinned Device ID, folder/ignore, root, and exact mount-binding preflight for every affected namespace before the helper key becomes current. A shared helper-key or TLS-pin rotation withholds signed capability success from an early installation until all required proposed-state confirmations permit the global commit; the early installation sees unavailable and then retries.
This is helper readiness, not app evidence. The released app does not call these
paths. No app-authored runtime request has established product upload evidence,
no response has passed a fresh post-authorization iPhone ItemFinished, and no
same-chain roundtrip exists. Upload, download, and roundtrip therefore remain
unset; cleanup remains separate from evidence. See
../docs/helper-runtime-packaging-readiness.md
for the exact support, privacy, compatibility, and rollback boundary.
Doctor mode — preflight checks with actionable failures (Compose reads .env for you):
docker compose run --rm vaultsync-notify --doctorValidates: Syncthing API reachable · API key valid · Device ID readable · relay health reachable · trigger endpoint sane. Each check retries with per-attempt timeouts to ride out transient jitter. It also diagnoses peer state — no remote device connected, or devices connected but no folder shared with them — as WARN lines (WARN <check name>, then an indented reason with the Syncthing-side fix) that never fail the doctor: an offline peer is everyday life, not a setup error.
Runtime healthcheck — the image's HEALTHCHECK runs vaultsync-notify --healthcheck, validating real readiness (Syncthing API, credentials, Device ID, relay health), not just process liveness. Peer state is deliberately excluded here: a legitimately offline peer must never flip the container to unhealthy.
Version — vaultsync-notify --version prints the installed helper version (needs no configuration). The installer shows old → new on upgrades. Docker installs pull the reviewed 2.0.2 tag and run only its resolved local content ID; failed pulls do not fall back to a stale tag. Binary installs select the newest published notify-v* release, require its SHA256SUMS, replace the binary, and restart the service. Future Docker upgrades require another reviewed version tag or an explicit VAULTSYNC_NOTIFY_IMAGE override.
Diagnostics pairing comparison — after the app has authenticated the
pending helper acceptance, run diagnostics-docker.sh list locally. The
pending row includes the exact D022 transcript= fingerprint that must match
the value in the app before activation. It is local comparison output, not a
credential; do not redirect it into service logs or support bundles. Active
rows omit it.
| Symptom | Fix |
|---|---|
401/403 / permission reading the key |
Wrong / unreadable Syncthing API key |
| Relay timeouts, DNS, connection refused | Relay unreachable |
| Subscribed but no wake-ups | APNs not registered |
| Anything else | End-to-end issue matrix |
Cloud Relay accelerates server → iPhone by waking VaultSync when the homeserver has outgoing changes. The container watches two Syncthing event types and ignores everything else:
LocalIndexUpdated— a direct change on the homeserver itself.FolderCompletionwithneedItems > 0orneedBytes > 0— a remote peer is behind and should be woken.
iPhone → server stays foreground-first on iOS: open VaultSync to push local edits back. Background refresh may help opportunistically. The released product has no controlled helper upload path: the optional, unpublished diagnostics endpoints above have no released app caller and set no upload evidence. See the Cloud Relay spec for the protocol.
cd notify
go build -o vaultsync-notify # native binary
docker build -t vaultsync-notify . # Docker imageOnly the Syncthing Device ID is sent to the relay, as a routing identifier. No file names, folder names, file sizes, or other metadata leave your server.
Operational logs contain fixed states, error categories, status codes, bounded counts, and durations. They do not contain Device IDs, folder IDs, event markers, local or Relay endpoint URLs, config paths, Syncthing API keys, pairing/namespace values, or raw request/response bodies. The default Trigger-v1 helper keeps the API key and watched-folder selection in process memory. Only the explicitly configured diagnostics runtime creates its separate protected credential and namespace-mapping state described above; that state is never synchronized or sent to Cloud Relay.
Licensed MPL-2.0.