S3 Sync
unlistedby fabian
Minimal AWS S3 sync plugin for Obsidian.
S3 Sync
Minimal AWS S3 sync plugin for Obsidian.
This rebuild intentionally keeps the plugin small:
- AWS S3 only — no R2, RustFS, WebDAV, Dropbox, OneDrive, or other providers
- Sync only — no snapshot backup system
- No encryption layer — plaintext objects in S3
- Three-way reconciliation — local vault, remote S3 state, and the last successful sync baseline are compared on every run
- Conflict-safe — conflicting edits produce
LOCAL_andREMOTE_files instead of silently overwriting data - Protect-modify guard — sync aborts when too large a share of the already-synced files would change at once
- No desktop-only runtime dependency — built around Obsidian APIs, IndexedDB, and web APIs rather than Node/Electron modules
What it does
The plugin performs bi-directional vault sync against a single AWS S3 bucket. It keeps a local IndexedDB journal so it can compare:
- your current local vault state
- the current remote S3 state
- the last successful sync baseline
That lets it detect uploads, downloads, deletions, and conflicts without a separate remote manifest.
Quick start
- Open Settings → S3 Sync.
- Enter your AWS region, bucket, access key ID, and secret access key.
- Click Test connection.
- Enable sync, optional auto-sync, and Sync on startup if you want a run when Obsidian opens.
- Review Exclude patterns if you do not want parts of
.obsidian/to propagate across devices.
Settings
The settings surface is intentionally small:
| Setting | Description |
|---|---|
| Region | AWS region for the bucket, for example eu-central-1. |
| Bucket | Name of the S3 bucket that stores the synced vault. |
| Access key ID | AWS access key used for S3 requests. |
| Secret access key | AWS secret access key used for S3 requests. |
| Test connection | Verifies credentials and bucket access with a lightweight S3 request. |
| Enable sync | Master switch for bi-directional vault sync. |
| Auto-sync | Runs sync on a fixed interval. |
| Sync interval | Interval for auto-sync: 1, 2, 5, 10, 15, or 30 minutes. |
| Sync on startup | Runs one sync after the vault finishes loading. |
| Abort if changed files exceed threshold | Aborts sync when the share of already-synced files that would change exceeds the threshold. The first sync to a destination is exempt; use 100 to disable. |
| Exclude patterns | One glob pattern per line for files or folders that should never be synced. Empty by default. The former defaults were .trash/**, now unreachable anyway, and **/workspace*, which was meant for .obsidian/workspace.json but could only ever match ordinary notes such as projects/workspace-plan.md — add it back yourself if you want that. |
| Sync hidden folders | One glob per line opting dot-prefixed folders into sync, for example .claude/**. Empty by default. |
| Reset sync journal | Clears remembered baselines for the current bucket and region so the next sync starts fresh against that destination. |
The plugin always excludes its own folder from sync, including data.json:
.obsidian/plugins/s3-sync/
Git metadata under any .git path is also always excluded.
Hidden folders
Obsidian's vault index never surfaces dot-prefixed files or folders, so by default they cannot sync at all. Sync hidden folders opts specific ones in:
.claude/**
.codex/**
Matching files are enumerated and transferred through the vault adapter rather than the vault API. They therefore sync as ordinary S3 objects while remaining invisible to Obsidian — they never become notes, never appear in search, and never enter the graph. This is the intended way to carry per-vault tooling config to machines that receive the vault only over S3.
Every glob must name a concrete hidden root; a bare ** or a visible folder is rejected, because discovering hidden files anywhere in the vault would mean walking the entire tree on every cycle. Patterns are accepted in exactly one shape — a concrete dot-prefixed folder followed by at least one more component, such as .claude/**. A bare .claude, a wildcard root, backslashes, and any . or .. segment are refused with the correction in the message rather than repaired, because a pattern decides which files leave the machine. .git, .trash, and the Obsidian config folder are refused at any depth, whatever the glob says: syncing a .git directory between machines corrupts repositories.
Two behaviours differ from ordinary notes. Remote deletions of hidden files always go to the vault's local .trash folder, because the adapter has no access to Obsidian's trash preference. And nesting is walked to a fixed depth of 16, which stops a symlinked folder from recursing forever.
Note that this setting lives in data.json, which is itself never synced, so it has to be set once per device.
Permissions and data access
This plugin is a sync tool, so by design it enumerates vault files and reads or writes the ones that fall inside its sync scope.
What the plugin reads
| API / storage | Why it is used |
|---|---|
vault.getFiles() | Enumerates vault files so the planner can discover local state. |
vault.read() / vault.readBinary() | Reads file contents before upload and when hashing ambiguous local changes. |
| IndexedDB journal | Loads per-file baselines, conflict records, and sync metadata from earlier successful runs. |
What the plugin writes
| Destination | What is stored there |
|---|---|
| S3 bucket root | Synced vault files as normal S3 objects, plus content-fingerprint metadata. |
| Local vault | Downloaded files, updated files, parent folders created as needed, and LOCAL_ / REMOTE_ conflict artifacts. |
| Local vault trash | Files deleted remotely are removed through Obsidian's trash flow, respecting the user's deleted-files preference. Allowlisted hidden files are an exception and always go to the local .trash folder. |
| IndexedDB | Per-file sync baselines, unresolved conflict records, and metadata such as the last successful sync time. |
data.json | Plugin settings such as AWS credentials, sync toggles, interval, threshold, and exclude patterns. |
What leaves your device
- Only traffic to the configured AWS S3 bucket for connection tests, listings, uploads, downloads, and deletes.
- Vault file contents for in-scope files.
- Object metadata written by the plugin: content fingerprint.
- No telemetry, analytics, crash reporting, or update polling.
What is not included
- No encryption layer. Objects are stored in S3 as plaintext payloads.
- No access outside the current vault.
- No raw filesystem access beyond Obsidian's own vault adapter, which is used only for paths matching a configured hidden-path glob.
- No Node.js shell, filesystem, or Electron APIs.
Important: nothing under
.obsidian/is ever synced — not the config files, not other plugins'data.json, not the workspace. Obsidian's vault index does not expose dot-prefixed paths, and the config folder is additionally on the never-syncable list, so no exclude pattern is needed and no hidden-path glob can opt it back in.
Conflict behavior
When both local and remote changed in incompatible ways, the plugin keeps both copies:
- local version →
LOCAL_<filename> - remote version →
REMOTE_<filename>
You resolve the conflict manually, keep the final file you want, and sync again.
Deleting both artifact files without recreating the original restores the remote copy on the next sync — dismissing a conflict never deletes anything.
Multi-device behavior
- Each device keeps its own IndexedDB journal.
- Sync decisions compare local state, remote S3 state, and the last successful baseline remembered on that device.
- If two devices modify the same file independently, the plugin creates
LOCAL_andREMOTE_copies instead of silently picking one side.
Bucket layout
Files are stored directly at the bucket root as normal S3 objects. The plugin writes content-fingerprint metadata for sync bookkeeping.
Security and operational notes
- Plaintext objects in S3: if you need encryption at rest, configure AWS-side bucket encryption separately. The plugin itself does not encrypt payloads.
- Scheduled sync requires the app to be active: mobile operating systems may suspend background work, so iOS and Android users should expect sync to run while Obsidian is open and active.
- Changing bucket or region is effectively a new destination: sync is blocked until you explicitly use Reset sync journal in Advanced settings, which prevents stale baselines from driving the wrong plan against a different remote.
Commands
- Sync now
- Open settings
FAQ
Does this work on mobile?
It is designed to. The plugin avoids Node/Electron APIs and uses Obsidian APIs plus browser features such as IndexedDB and Web Crypto. The main practical caveat is that iOS and Android may suspend background activity when Obsidian is not foregrounded.
Can I use this alongside Obsidian Sync?
It is not recommended. Running two sync systems against the same files increases the chance of races and conflicts.
What files are excluded by default?
There are no editable defaults — Exclude patterns starts empty. Independently of any setting, nothing dot-prefixed syncs unless a hidden-path glob opts it in, and .git, .trash, and the Obsidian config folder (including this plugin's own data.json) can never be opted in at all.
Development
npm install
npm run lint
npm run build
npm test
BRAT releases
This repo is laid out in a BRAT-friendly way: the release assets BRAT needs are the root-level manifest.json, main.js, and styles.css.
A GitHub Actions workflow at .github/workflows/release.yml automates that release flow:
Commits on main are not delivered to installed BRAT copies by themselves. After changes are ready, publish a new version before expecting Obsidian to receive them.
- bump
manifest.json,package.json, andpackage-lock.jsonto the version you want to ship - create and push a matching tag, for example
0.1.1orv0.1.1 - the workflow will lint, test, build, create a GitHub release, and attach:
manifest.jsonmain.jsstyles.css
The workflow fails if the release tag version does not match manifest.json.
Credits
This plugin is a stripped-down reinterpretation of ceilaolabs/obsidian-s3-sync-and-backup — the original ("OG") project that inspired it. That repo is the full-featured take (multiple storage providers, encryption, scheduled backups). This one deliberately keeps a much smaller surface: AWS S3 only, sync only, no encryption. Several safety and correctness ideas here (weak-ETag normalization, destination-fingerprint / stale-journal protection, the destructive-plan block, and the Reset sync journal action) are borrowed from it. Credit for the original concept goes to its authors.
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.