R2 API token usage

SSS uses an S3-compatible API to interact with Cloudflare R2. The API token you create in Cloudflare is used directly — there is no intermediate server. Every request is made from the Obsidian client on your device.

Request types per sync

A typical sync cycle issues the following R2 API calls:

OperationAPI callWhen
List remote filesListObjectsV2Every sync — one call (paginated for large vaults)
Download fileGetObjectPer file pulled from remote
Upload filePutObjectPer file pushed to remote
Delete fileDeleteObjectPer deletion propagated to remote
Get remote ETagHeadObjectPer pushed file (to store ETag in prevSync)
Sentinel read/writeGetObject / PutObjectSmart Sync only — one small object per sync

Cloudflare R2 free tier limits

ResourceFree allowanceTypical vault usage
Storage10 GB / monthMost vaults < 500 MB
Class A operations (writes, lists)1,000,000 / monthLow — only changed files are written
Class B operations (reads)10,000,000 / monthVery low with Smart Sync (sentinel-driven)
Smart Sync reduces request count significantly. Rather than polling R2 on a fixed interval, it writes a tiny sentinel object after each sync. Other devices detect this via a periodic check (default every 30 s) and only pull if the sentinel changed — avoiding redundant ListObjectsV2 calls.

Encryption internals

SSS supports two encryption methods, selectable per-vault. Both use client-side encryption — files are encrypted before upload and decrypted after download.

OpenSSL AES-CBC (openssl-base64)

  • Content encrypted with AES-256-CBC, OpenSSL-compatible format.
  • File names are encoded as base64url but not encrypted — the file path structure is visible on the remote.
  • Compatible with standard OpenSSL tooling for manual recovery if needed.
  • Salt and IV are prepended to each file, following the Salted__ header convention.

rclone Salsa20 (rclone-base64)

  • Content encrypted with Salsa20+Poly1305 — same algorithm used by rclone's crypt remote.
  • File names are also encrypted and randomised — directory structure is hidden on the remote.
  • Compatible with rclone for manual recovery using the same password.
  • Implemented in a dedicated Web Worker to avoid blocking the UI thread on large files.
Do not switch methods mid-vault. Changing the encryption method after syncing means the remote files were encrypted with the old method. The plugin will detect the mismatch at startup and refuse to sync. Clear the remote, reset sync history, and sync fresh with the new method.

3-way sync engine

The sync engine compares three states for each file path:

Local

Current state of the file on this device's vault.

Remote

Current state of the encrypted blob in R2.

prevSync

Snapshot from the last successful sync, stored in a local SQLite DB.

Change detection uses ETag for remote comparisons and mtime + size for local comparisons. ETags are stored in the prevSync record after each push to avoid re-uploading unchanged encrypted files (whose size and mtime differ from plaintext).

Decision matrix

Local changed?Remote changed?Decision
NoNoSkip (no_change)
YesNoPush local → remote
NoYesPull remote → local
YesYesConflict — resolved per setting (keep_newer, keep_larger, etc.)
Deleted locallyUnchangedDelete remote
UnchangedDeleted remotelyDelete local
New (no prevSync)AbsentPush (first upload)
AbsentNew (no prevSync)Pull (first download)

Smart Sync & sentinel mechanism

Smart Sync uses a lightweight sentinel object in R2 (.sss-sentinel) to notify other devices of a completed sync without requiring a persistent WebSocket connection.

1

Idle trigger

After smartSyncIdleSeconds (default 7 s) of editor inactivity, a sync is triggered. This is debounced on every editor-change event.

2

Sentinel write

After a successful sync, the plugin writes a small JSON object to R2 containing a timestamp and device ID. Sentinel writes are skipped for state_aware triggers to prevent A→B→A cascade loops.

3

Sentinel poll

Other open devices poll the sentinel every 30 s. If the sentinel's timestamp is newer than the last seen value and the device ID differs (i.e., another device wrote it), a state_aware sync is triggered to pull changes.

4

Visibility-aware polling

The poll interval is paused when the document is hidden (app backgrounded) and resumed immediately on visibility. A one-shot poll fires on foreground to catch missed changes without waiting 30 s.


Self-hosting the pairing relay

The pairing relay is a minimal Cloudflare Worker that holds an encrypted credential bundle for up to 10 minutes. It is fully open source.

Repository: github.com/xensenx/sss-relay

Deploy it as a Cloudflare Worker, then point SSS to your URL under Settings → Advanced → Custom Relay URL. The relay never sees plaintext credentials — the bundle is encrypted client-side with a key derived from the pairing code before being sent.

Relay API surface

EndpointMethodPurpose
/createPOSTStore encrypted bundle, receive pairing code
/consume/:codePOSTRetrieve and delete bundle by code
/healthGETRelay liveness check

Local database

SSS uses IndexedDB (via the idb-keyval-style abstraction in database.ts) to persist sync state. The database is namespaced by vault ID so multiple vaults on the same device do not interfere.

Key tables:

  • prev_sync_records — one row per synced file: path, size, mtime, ETag.
  • sync_history — timestamped log of past sync stats for diagnostics.
  • meta — plugin version, last success/failure timestamps.

The database location is managed by Obsidian's plugin data API and is not directly accessible from the filesystem. Use Reset Sync History in Danger Zone to clear prev_sync_records, which forces a full comparison on the next sync.


Building from source

# Clone the repository
git clone https://github.com/xensenx/Secure_Smart_Sync.git
cd Secure_Smart_Sync

# Install dependencies
npm install

# Development build (watch mode)
npm run dev

# Production build
npm run build

The built output (main.js, manifest.json, styles.css) is placed in the project root. Copy these into your vault's .obsidian/plugins/Secure-Smart-Sync/ directory to test.