Point it at a folder of Markdown files and edit your notes from any browser — with a CodeMirror editor, live preview, wikilinks, an interactive graph, full-text search, GitHub sync (incl.
概要
Point it at a folder of Markdown files and edit your notes from any browser — with a CodeMirror editor, live preview, wikilinks, an interactive graph, full-text search, GitHub sync (incl.
README
What is this?
WebObsidian is a web application that gives you an Obsidian-like
experience over a real folder of Markdown files living on your server. Your vault is
100% compatible with an existing Obsidian vault (including the .obsidian/ folder) — you
can edit the same files from the Obsidian desktop app and from the web, side by side.
It is single-user and self-hosted: one master password protects the whole app, all
configuration lives in a plain data/settings.json (no database engine), and the entire
stack runs from a single docker compose up.
Why? To access and edit your knowledge base from any browser, on any device, while keeping full ownership of your files — and to let AI agents read/write your vault through a safe, scoped REST API.
Repo gốc: https://github.com/xnohat/webobsidian
✨ Features
- 📝 Editor & rendering — CodeMirror 6 with live / source / reading views; wikilinks
[[note]], embeds![[file]], tags#tag, callouts, task lists, KaTeX math and Mermaid diagrams. - 🎨 Canvas & Excalidraw — visual canvas for connecting notes, cards, and media; Excalidraw whiteboard integration for freeform drawing.
- 🕸️ Graph view — GPU-accelerated (PixiJS) force-directed graph built from your wikilinks, with fly-to node search and highlighting.
- 🔗 Backlinks & outline — right sidebar tab strip: Backlinks (linked and unlinked mentions), Outgoing links (resolved/unresolved), Tags and Outline.
- 🔍 QMD search — fast full-text + fielded search (
tag:,path:,title:), fuzzy + prefix matching, incremental indexing, persisted to disk for fast startup. - 🔄 GitHub sync — native
gitpull / commit / push with Git LFS for large attachments, optional auto-sync, and per-file version history (browse & restore). - 🔐 Secure sign-in — a scrypt-hashed master password, optional authenticator-app 2FA with one-time recovery codes, and passwordless Passkeys/WebAuthn; sessions use an httpOnly cookie and are revoked after sensitive account changes.
- 🌐 Public sharing — turn any note into a read-only, server-rendered (SEO-friendly)
public page at
/share/, optionally password-protected. - 🤖 Agent API — scoped API keys (
read/write/search) let AI agents work with the vault over REST at/api/v1. See docs/AGENT_API.md. - 🧩 Community plugins — CSP-safe loading with commands, hotkeys, custom views, settings, ribbon/status items, Markdown processors, plugin data and clear diagnostics. See the compatibility matrix.
- 📱 Responsive / mobile — drawer sidebars, edge-swipe, an on-keyboard formatting toolbar, and touch-friendly targets, à la Obsidian Mobile.
- 📋 Bases — Obsidian Bases-style database views (table/task/list) over your notes with a formula expression engine.
- 🗃️ Pure-JSON config — everything lives in
data/settings.json. No database. - 🐳 Docker — one command to run the whole stack.
🚀 Quick start (Docker)
git clone https://github.com/tuongzaza/obsidian.git webobsidian
cd webobsidian
cp .env.example .env # edit VAULT_HOST_PATH, set WEBOBSIDIAN_PASSWORD
docker compose up -d --build
# open http://localhost:8787
Out of the box it serves the bundled ./sample-vault, so the stack boots immediately. All
deployment settings live in .env (git-ignored) — you never edit the tracked
docker-compose.yml, so a git pull / redeploy keeps your config and vault mapping intact.
🖥️ Electron desktop app
Prefer a native app? Grab an installer from the Releases page — available for macOS / Windows / Linux:
| Platform | Download |
|---|---|
| macOS | .dmg / .zip — arm64, x64 |
| Windows | NSIS / portable .exe — x64, arm64, ia32 |
| Linux | .AppImage / .deb — x64, arm64 |
The Electron app bundles the same React UI and an embedded loopback-only service, so it
keeps full web feature parity while owning a separate local vault. On first launch,
choose a vault folder. The hosted web app and desktop never mirror the open note or edit
the same files implicitly; configure the same repository under Settings → GitHub Sync
only when you want their vaults synchronized. Build with npm run desktop:dist; see
desktop/README.md.
Apps are currently unsigned, so the operating system may warn on first launch.
📱 Native Android app (prototype)
A Compose Multiplatform client lives in native-client/. It renders
natively through Skia/Android Canvas and uses Ktor/OkHttp for API access — no WebView
or Capacitor. Currently supports: server login (password + TOTP), vault tree, full-text
search, text editor with autosave, and adaptive two-column layouts.
Build from the repository root (requires JDK 21 + Android SDK 36):
npm run mobile:apk
# Output: native-client/composeApp/build/outputs/apk/debug/composeApp-debug.apk
See native-client/README.md for details and limitations.
🔑 Docker requires a strong
WEBOBSIDIAN_PASSWORDin.env. A bare-metal instance bound only to localhost can instead complete the one-time password setup in the browser. After login, enable authenticator 2FA and register Passkeys in Settings → Account.
Point it at your own vault
# .env
VAULT_HOST_PATH=/abs/path/to/your/ObsidianVault # must exist; bind-mounted to /vault
WEBOBSIDIAN_PASSWORD=use-a-strong-password
HTTP_BIND=0.0.0.0 # 127.0.0.1 to expose only to localhost
HTTP_PORT=8787
Then docker compose up -d --build. Your vault can be a plain folder or a git clone
(Git LFS is supported for attachments).
Behind a reverse proxy (TLS)
Set HTTP_BIND=127.0.0.1 so the app is only reachable from the host, then terminate TLS
with nginx / Caddy / Traefik in front of http://127.0.0.1:8787.
Passkeys require HTTPS (except on localhost). Set the canonical origin in .env, then
rebuild the container:
PUBLIC_BASE_URL=https://notes.example.com
PASSKEY_ORIGIN=https://notes.example.com
PASSKEY_RP_ID=notes.example.com
Changing PASSKEY_RP_ID later makes Passkeys registered for the previous domain unusable.
Large vaults & file watching
A fresh VPS ships a low fs.inotify.max_user_watches (often 8192), which a big vault
exceeds. WebObsidian auto-detects this and falls back to polling (works anywhere,
higher CPU). For lower CPU, raise the kernel limit and keep native watching:
sudo sysctl -w fs.inotify.max_user_watches=524288
echo 'fs.inotify.max_user_watches=524288' | sudo tee -a /etc/sysctl.conf
The search index (QMD) and link graph are kept in memory, so memory use scales with the
number of notes. The Docker image sets NODE_OPTIONS=--max-old-space-size=4096 (4 GB);
raise it to 8192 for very large vaults (e.g. 6k+ notes / multi-GB).
💻 Local development
Requires Node ≥ 20.19 and git (+ git-lfs if you use LFS).
npm install
npm run dev # server on :8787 + web dev server on :5173 (proxied)
# open http://localhost:5173
Production build (the server serves the built SPA):
npm run build
VAULT_PATH=./sample-vault npm start
# open http://localhost:8787
Useful scripts:
| Command | What it does |
|---|---|
npm run dev |
Run server + web together in watch mode |
npm run build |
Build the web SPA, then compile the server |
npm start |
Run the production server (serves built web) |
npm run typecheck |
Type-check both server and web workspaces |
npm test |
Run tests across server, web, and desktop |
npm run desktop |
Build web+server, then launch the Electron app |
npm run desktop:dist |
Build web+server, then package Electron installers |
npm run mobile:apk |
Build the native Android debug APK |
npm run native:test |
Run native-client (Kotlin) tests |
⚙️ Configuration
Docker env (.env, consumed by docker-compose.yml)
| Var | Default | Description |
|---|---|---|
VAULT_HOST_PATH |
./sample-vault |
Host path bind-mounted to /vault |
HTTP_BIND |
127.0.0.1 |
Host interface to publish on (127.0.0.1 = local only) |
HTTP_PORT |
8787 |
Host port mapped to container 8787 |
WEBOBSIDIAN_PASSWORD |
– | Seed/override the master password |
PUBLIC_BASE_URL |
– | Canonical HTTPS origin for share metadata |
PASSKEY_ORIGIN |
– | Canonical HTTPS origin used to register and verify Passkeys |
PASSKEY_RP_ID |
origin hostname | WebAuthn relying-party domain |
UPLOAD_MAX_BYTES |
67108864 |
Max upload size in bytes (default 64 MB) |
UPLOAD_MAX_CONCURRENT |
4 |
Max simultaneous uploads |
TRUST_PROXY |
true |
Proxy trust for X-Forwarded-Proto HTTPS detection |
WEBOBSIDIAN_WATCH |
auto |
auto (native + polling fallback) or polling |
App-level env (read by the server; Docker sets these inside the container)
| Var | Default | Description |
|---|---|---|
PORT |
8787 |
HTTP port |
HOST |
0.0.0.0 |
Listen address |
VAULT_PATH |
./sample-vault |
Path to the notes vault |
DATA_DIR |
./data |
Where settings.json + search index live |
ALLOWED_ROOTS |
– | Comma-separated roots the vault picker may browse |
WEBOBSIDIAN_PASSWORD |
– | Seed/override the master password |
PASSKEY_ORIGIN |
request origin | Canonical HTTPS WebAuthn origin |
PASSKEY_RP_ID |
origin hostname | WebAuthn relying-party domain |
WEBOBSIDIAN_WATCH |
auto |
File-watch mode: auto or polling |
NODE_OPTIONS |
--max-old-space-size=4096 |
Node heap size — raise for large vaults |
Everything else — git remote/token, API keys, plugins, theme — is configured in the
Settings UI and stored in data/settings.json.
🤖 Agent API
Scoped REST API for AI agents at /api/v1. Create an API key in Settings → API Keys,
then pass it as a header. Full reference: docs/AGENT_API.md.
🧩 Drop-in agent skill (no clone needed)
Want your coding agent (Claude Code, Codex, OpenCode, Cursor…) to work with your vault
directly? Copy the block below and paste it to your agent. It downloads the skill from
GitHub, asks for your URL + API key once, saves them to ~/.webobsidian/credentials.json,
and can then read/write/search your notes on request.
First create an API key in the app at Settings → API Keys (scopes
read/write/search).
SYSTEM / TASK — Install the "webobsidian" agent skill from GitHub, then set up credentials.
Do every step in order; do not skip the credential step.
STEP 1 — Download the skill into your runtime's skills directory
(Claude Code: ~/.claude/skills · Codex: ~/.codex/skills · OpenCode: ~/.opencode/skills):
SKILLS_DIR=~/.claude/skills # change for your runtime
mkdir -p "$SKILLS_DIR/webobsidian"
curl -fsSL https://raw.githubusercontent.com/tuongzaza/obsidian/main/docs/agent-skill/webobsidian/SKILL.md \
-o "$SKILLS_DIR/webobsidian/SKILL.md"
STEP 2 — Set up credentials (ASK ME; never echo the key back). Ask me for my WebObsidian
base URL (e.g. https://notes.example.com) and my API key (looks like wok_...), then:
mkdir -p ~/.webobsidian && chmod 700 ~/.webobsidian
printf '{ "baseUrl": "%s", "apiKey": "%s" }\n' "" "" > ~/.webobsidian/credentials.json
chmod 600 ~/.webobsidian/credentials.json
STEP 3 — Verify (do NOT print the key) and confirm ready:
BASE=$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.webobsidian/credentials.json")))["baseUrl"].rstrip("/"))')
KEY=$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.webobsidian/credentials.json")))["apiKey"])')
curl -s "$BASE/api/v1/health"
curl -s -H "X-API-Key: $KEY" "$BASE/api/v1/tags" | head
From now on, when I ask you to work with my WebObsidian / Obsidian vault, use the webobsidian skill.
Details & alternatives: docs/agent-skill/INSTALL.md · canonical skill: docs/agent-skill/webobsidian/SKILL.md.
KEY=wok_your_key_here
BASE=http://localhost:8787/api/v1
# list notes
curl -H "X-API-Key: $KEY" "$BASE/notes?limit=10"
# create / update a note
curl -X PUT -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"content":"# From the agent\n\nHello vault."}' \
"$BASE/notes/Agent/Generated.md"
# search (fielded queries supported: tag:, path:, title:)
curl -H "X-API-Key: $KEY" "$BASE/search?q=tag:idea%20graph&limit=5"
| Endpoint | Scope | Description |
|---|---|---|
GET /api/v1/notes |
read | List notes (paginated) |
GET /api/v1/notes/{path} |
read | Read a note + metadata |
PUT /api/v1/notes/{path} |
write | Create / overwrite |
PATCH /api/v1/notes/{path} |
write | Append content |
DELETE /api/v1/notes/{path} |
write | Move to trash |
GET /api/v1/search?q= |
search | QMD search |
GET /api/v1/backlinks?path= |
read | Backlinks for a note |
GET /api/v1/tags |
read | All tags with counts |
🏗️ Architecture
Monorepo with three npm workspaces (server, web, desktop) plus a Kotlin
Multiplatform native client:
webobsidian/
├── server/ # Express + TypeScript API
│ └── src/{routes,services,middleware,lib}
├── web/ # React + Vite SPA (built into server/public)
│ └── src/{components,lib,styles}
├── desktop/ # Electron desktop app (local-first, embeds server)
│ └── src/
├── native-client/ # Kotlin Multiplatform / Compose (Android prototype)
│ └── composeApp/
├── mobile/ # Convenience scripts for native Android builds
├── data/ # runtime: settings.json + search index (git-ignored)
├── docs/ # AGENT_API.md, PLUGIN_COMPATIBILITY.md, agent-skill
├── sample-vault/ # bundled demo vault for quick start
├── Dockerfile · docker-compose.yml · .env.example
┌──────────────────────── Browser (React SPA) ────────────────────────┐
│ CodeMirror 6 · Live Preview · Canvas · Excalidraw · Graph · Search │
└───────────────▲──────────────────────────────────┬──────────────────┘
│ REST + WebSocket │ static assets
┌───────────────┴──────────────────────────────────▼──────────────────┐
│ Server (Node 22 + Express + TypeScript) │
│ Auth gate │ Vault FS │ QMD Search │ Git Sync │ API Gate │ Plugins │
└──────┬──────────────┬───────────┬────────────┬───────────────┬───────┘
settings.json Vault dir Search index GitHub repo plugins dir
(JSON config) (.md+attach) (in-mem/disk) (git + LFS) (.obsidian/plugins)
Tech stack: Node 22 · Express · TypeScript · React 18 · Vite · CodeMirror 6 · PixiJS · Excalidraw · unified/remark/rehype · MiniSearch (QMD) · simple-git + git-lfs · scrypt + JWT · WebAuthn · Zustand · Electron · Kotlin/Compose Multiplatform · Docker.
See PRD.md §2 for the full design.
🔒 Security notes
- Master password is scrypt-hashed; the JWT secret is auto-generated.
- API keys are hashed at rest and scoped (
read/write/search) with per-key rate limiting and audit logging. - All file paths are guarded against traversal; the vault picker is confined to
ALLOWED_ROOTS. - Secrets (git token / API keys) live in
data/settings.jsonon the server — mount/dataas a private volume and keep it off version control. Change the default password. - The Electron desktop app binds to
127.0.0.1only, enablescontextIsolationand renderer sandbox, and disables Node integration.
🗺️ Compatibility & scope
- ✅ Works directly on an existing Obsidian vault, including
.obsidian/config. - ⚠️ Single-user (v1) — no real-time multi-user collaborative editing yet.
- ⚠️ Git sync replaces Obsidian Sync/Publish.
- ⚠️ Community-plugin support targets the public browser-compatible Obsidian API; Electron/Node and private-internal plugins are not supported. See Community plugin compatibility.
🤝 Contributing
Contributions are welcome! A few house rules from CLAUDE.md:
- Follow PRD.md. It is the source of truth for design. Changing scope means updating the PRD first (with a changelog bump), then the code.
- Keep IMPLEMENTATION_PLAN.md in sync — flip checkboxes and add a progress-log line as you work.
- TypeScript everywhere; avoid
any. Runtime config is JSON only (no DB engine). - Never log secrets/tokens; hash before storing; guard against path traversal.
Run npm run typecheck before opening a PR.
📄 License
MIT © xnohat
推奨ツール
別のキーワードを試すか、フィルタを外してください。
インストール
npx skillfish add tuongzaza/obsidian