Search...Search plugins and themes...
⌘K
Sign in
  • Get started
  • Download
  • Pricing
  • Enterprise
  • Account
  • Obsidian
  • Overview
  • Sync
  • Publish
  • Canvas
  • Mobile
  • Web Clipper
  • CLI
  • Learn
  • Help
  • Developers
  • Changelog
  • About
  • Roadmap
  • Blog
  • Resources
  • System status
  • License overview
  • Terms of service
  • Privacy policy
  • Security
  • Community
  • Plugins
  • Themes
  • Discord
  • Forum / 中文论坛
  • Merch store
  • Brand guidelines
Follow us
DiscordTwitterBlueskyThreadsMastodonYouTubeGitHub
© 2026 Obsidian

Pumice

Ji-ho LeeJi-ho Lee73 downloads

Sync, version, and publish your vault over your own self-hosted gRPC server.

Add to Obsidian
  • Overview
  • Scorecard
  • Updates17

🇺🇸 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.

Overview

  • Client: TypeScript, Obsidian community plugin (this repository)
  • Server: Python (asyncioreactor + grpc.aio + Twisted), see pumice-server
  • Transport: gRPC-Web (HTTP/2 multiplexing, bidirectional streaming) — many files are sent concurrently over a single connection instead of one RPC per file. When the server is reachable over TLS, uploads instead stream directly over a single fetch() request (no batching, no buffering the whole payload in memory); otherwise they fall back to the gRPC-Web path above.
  • Auth: a static token stored in Obsidian's own secret storage (App#secretStorage, desktop and mobile alike, no platform-specific code needed)

Key features:

  • Vault file sync (delta comparison, only changed files are uploaded/downloaded)
  • Sync history browsing and file recovery (syncHistoryModal, fileRecoveryModal)
  • Automatic local snapshots with retention (localSnapshotStore)
  • Selective publishing of chosen folders (publishModal)
  • Localization support (Korean/English, src/locales)

Requirements

  • Node.js (with npm)
  • protoc (verified with 3.21.12)
  • Obsidian 1.13.4+ (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.

Building

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.

Releasing

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.

Installing locally in Obsidian for testing

  1. Run npm run build to generate main.js.
  2. Create a .obsidian/plugins/pumice/ folder in your vault and copy main.js, manifest.json, and styles.css into it.
  3. Enable Pumice under Settings → Community plugins in Obsidian.

Regenerating the gRPC-Web stubs

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 system
  • protoc-gen-js (node_modules/.bin/protoc-gen-js, installed via npm install)
  • the 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

Settings

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: true in 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.

Project structure

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

Contributing

  1. Fork the repository and create a branch.
  2. Run npm run lint after your changes to make sure there are no type errors.
  3. Keep commit messages concise and focused on the reason for the change.
  4. Open a Pull Request. Include screenshots for UI changes.

Please use GitHub Issues to report bugs or suggest features.

Support

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.

License

BSD 3-Clause License

HealthExcellent
ReviewCaution
About
Sync your vault with a self-hosted pumice-server using gRPC‑Web streaming for concurrent, delta-only file transfers. Browse sync history and recover files, keep local snapshots with retention, publish chosen folders, and authenticate via a static token in the OS keychain.
SyncingBackupPublishing
Details
Current version
0.0.30
Last updated
5 hours ago
Created
4 months ago
Updates
17 releases
Downloads
73
Compatible with
Obsidian 1.13.4+
Platforms
Desktop, Mobile
License
BSD-3-Clause
Report bugRequest featureReport plugin
Author
Ji-ho LeeJi-ho Leesearch5
GitHubsearch5
  1. Community
  2. Plugins
  3. Syncing
  4. Pumice

Related plugins

YAOS

Simple real-time sync powered by your own Cloudflare Worker.

GitHub

GitHub Sync

Sync vault to personal GitHub.

Remotely Save

Sync notes between local and cloud with smart conflict: S3, Dropbox, webdav, OneDrive, Google Drive, Box, pCloud, Yandex Disk, Koofr, Azure Blob Storage.

Differential ZIP Backup

Back our vault up with lesser storage.

GitHub

GitHub Gitless Sync

Sync a GitHub repository with vaults on different platforms without requiring git installation

Settings profiles

Create various global settings profiles, that sync between vaults.

Remotely Sync

Security fixes for the remotely-save unofficial plugin allowing users to synchronize notes between local device and the cloud service. Not backwards compatible.

Fast Note Sync

Real-time sync of your vaults across server, mobile, and web; shareable with anyone; supports REST and MCP integrations to build your personal AI knowledge base.

Share Note

Instantly share/publish a note, with the full theme and content exactly like you see in Obsidian. Data is shared encrypted by default, and only you and the person you send it to have the key.

Git

Integrate Git version control with automatic backup and other advanced features.