TO

tuongzaza/obsidian

开发工具
83 stars 质量 70 趋势 70

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 git pull / 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_PASSWORD in .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.json on the server — mount /data as a private volume and keep it off version control. Change the default password.
  • The Electron desktop app binds to 127.0.0.1 only, enables contextIsolation and 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:

  1. 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.
  2. Keep IMPLEMENTATION_PLAN.md in sync — flip checkboxes and add a progress-log line as you work.
  3. TypeScript everywhere; avoid any. Runtime config is JSON only (no DB engine).
  4. Never log secrets/tokens; hash before storing; guard against path traversal.

Run npm run typecheck before opening a PR.


📄 License

MIT © xnohat


View this README on GitHub

推荐工具

换一个关键词,或者移除筛选条件。

安装

npx skillfish add tuongzaza/obsidian