Ji-ho Lee73 downloadsSync, version, and publish your vault over your own self-hosted gRPC server.
🇺🇸 English | 🇰🇷 한국어
An Obsidian community plugin that syncs your vault with a self-hosted gRPC server (pumice-server — required, run it yourself). The goal is to sync instantly, no matter how many files are in the vault.
asyncioreactor + grpc.aio + Twisted), see
pumice-serverfetch() request (no batching,
no buffering the whole payload in memory); otherwise they fall back to the gRPC-Web path above.App#secretStorage, desktop
and mobile alike, no platform-specific code needed)Key features:
syncHistoryModal, fileRecoveryModal)localSnapshotStore)publishModal)src/locales)protoc (verified with 3.21.12)manifest.json's minAppVersion). The settings tab renders entirely via
the declarative settings API (getSettingDefinitions()), so it's searchable from Obsidian's
own settings search; there's no legacy/imperative fallback UI to keep in sync.npm install
# Development mode (watch)
npm run dev
# Production build
npm run build
# Type-check only
npm run lint
main.js is generated by esbuild from src/. For releases, it's built alongside
manifest.json and styles.css and attached as a GitHub Release artifact.
Pushing a tag runs .github/workflows/release.yml, which
builds the plugin and creates a GitHub Release with main.js, manifest.json, and
styles.css attached (this is also what tools like BRAT, and the official Community Plugins
installer, expect to find). The tag must match manifest.json's version exactly, with no
v prefix.
Bump the version with npm version, which syncs manifest.json and versions.json (via
scripts/version-bump.mjs, wired up as the version lifecycle script) and creates a matching
git tag (.npmrc disables npm's default v prefix):
npm version patch # or minor / major
git push --follow-tags
versions.json maps each released plugin version to the minAppVersion it required at the
time — Obsidian's installer uses it to pick a compatible release for users on older app
versions, which matters once this plugin is submitted to the Community Plugins list.
npm install fetches the protoc-gen-grpc-web binary via a postinstall script, and
npm run dev / build / lint each generate src/generated/ from sync.proto first
(via pre* scripts) if it isn't there yet — the only thing you need installed yourself is
protoc. See Regenerating the gRPC-Web stubs for details.
npm run build to generate main.js..obsidian/plugins/pumice/ folder in your vault and copy main.js,
manifest.json, and styles.css into it.src/generated/ (sync_pb.js, sync_pb.d.ts, SyncServiceClientPb.ts, SyncServiceClientPb.d.ts)
is generated from sync.proto. The .js/.ts implementation files are gitignored and generated
automatically the first time you run npm run dev/build/lint (via scripts/ensure-generated.mjs,
wired up as a pre* script), so a fresh clone doesn't need a manual step. The two .d.ts files
are committed, though (SyncServiceClientPb.d.ts is derived from the .ts via
tsc --declaration --emitDeclarationOnly as part of npm run proto:gen) — this is deliberate:
external tools that lint/typecheck this repo without running npm install/generation first can
still resolve real types for every pb.*/SyncServiceClient reference from just the .d.tss,
instead of every such reference collapsing to any and cascading into hundreds of unrelated
no-unsafe-* findings (confirmed locally — with only the .d.ts files present, eslint .
reports zero findings related to this).
The pre* scripts only run generation if the .ts implementation is missing though —
if you modify sync.proto, regenerate the stubs explicitly and commit the two updated .d.ts
files:
npm run proto:gen
Prerequisites:
protoc installed on your systemprotoc-gen-js (node_modules/.bin/protoc-gen-js, installed via npm install)protoc-gen-grpc-web binary (bin/protoc-gen-grpc-web, v1.5.0 — too large for the repo,
so npm install fetches it automatically via scripts/fetch-protoc-gen-grpc-web.mjs, a
postinstall script). If your platform/arch isn't supported by the script or the download
fails, it prints a fallback link to the
grpc-web releases page so you can fetch
it manually.npm run proto:gen runs the underlying command directly:
protoc \
--plugin=protoc-gen-js=./node_modules/.bin/protoc-gen-js \
--js_out=import_style=commonjs,binary:./src/generated \
--plugin=protoc-gen-grpc-web=./bin/protoc-gen-grpc-web \
--grpc-web_out=import_style=typescript,mode=grpcweb:./src/generated \
--proto_path=. \
sync.proto
| Setting | Default | Description |
|---|---|---|
| serverHost | localhost | Pumice server address |
| serverPort | 8080 | HTTP + gRPC-Web port |
| useTls | false | Use TLS (recommended for remote servers) |
| deviceName | Obsidian Client | Name identifying this device |
| userName | Obsidian User | User name |
| syncFiles | true | Whether to sync files |
| syncBookmarks | true | Whether to include bookmarks (.obsidian/bookmarks.json) |
| ignorePatterns | see below | Path patterns excluded from sync |
| autoSync | false | Enable automatic sync |
| syncIntervalSeconds | 60 | Auto-sync interval (seconds) |
| syncOnStartup | false | Sync on startup |
| conflictResolution | manual | Conflict resolution strategy (manual / server-wins / client-wins / merge) |
| enableE2EE | false | Enable end-to-end encryption |
| publishIncludeFolders / publishExcludeFolders | - | Folders to include/exclude when publishing |
| localSnapshotIntervalMinutes | 5 | Local snapshot interval (minutes) |
| localSnapshotKeepDays | 7 | Local snapshot retention (days) |
Default exclude patterns (ignorePatterns / publishExcludeFolders):
.obsidian/workspace
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.obsidian/cache
.obsidian/plugins/pumice
.trash
The vault's folder name is its identity on the server. There's no separate vault ID — the vault's folder name is used as-is to key everything server-side (sync, publish, version history). Every device syncing the same vault needs a folder with the exact same name; a mismatch isn't rejected, it just syncs as an unrelated vault. The settings tab shows the current vault's name for this reason.
"Publish current file" requires
publish: truein the note's frontmatter. Folder-level inclusion (publishIncludeFolders) doesn't need it, but the single-file force-publish action won't upload a file until its frontmatter says so — otherwise a file could go live on the server yet silently fall out of scope on the next folder-wide publish scan, which is frontmatter-driven.
pumice/
├── src/
│ ├── main.ts # Plugin entry point
│ ├── settings.ts # Settings types and defaults
│ ├── settingsTab.ts # Settings panel UI
│ ├── syncClient.ts # gRPC sync client
│ ├── syncHistoryModal.ts # Sync history UI
│ ├── fileRecoveryModal.ts # File recovery UI
│ ├── publishModal.ts # Selective publish UI
│ ├── localSnapshotStore.ts # Local snapshot management
│ ├── contentHashCache.ts # Persists per-file content hashes (mtime+size keyed)
│ ├── concurrency.ts # mapWithConcurrency / streamWithConcurrency helpers
│ ├── diffView.ts # File diff view
│ ├── swipeNavigation.ts # Mobile swipe navigation
│ ├── tokenStore.ts # Auth token storage (App#secretStorage)
│ ├── errorMessage.ts # Error-to-string helper
│ ├── i18n.ts, locales/ # Localization strings
│ └── generated/ # Generated by protoc
├── bin/protoc-gen-grpc-web # Fetched by postinstall (too large for the repo)
├── scripts/
│ ├── fetch-protoc-gen-grpc-web.mjs # Downloads bin/protoc-gen-grpc-web on npm install
│ ├── gen-proto.mjs # Runs protoc, used by `npm run proto:gen`
│ ├── ensure-generated.mjs # Generates src/generated/ if missing (pre-dev/build/lint)
│ └── version-bump.mjs # Syncs manifest.json/versions.json, run by `npm version`
├── main.js # Generated by esbuild
├── sync.proto # gRPC schema
├── manifest.json # Obsidian plugin manifest
├── versions.json # Plugin version → minAppVersion map
└── esbuild.config.mjs # Build configuration
npm run lint after your changes to make sure there are no type errors.Please use GitHub Issues to report bugs or suggest features.
If you'd like to sponsor this project, reach out at [email protected]. Sponsorships make a real difference in how much time can go into development.