A self-hosted, Obsidian-compatible web app for your Markdown "second brain".
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. Git LFS), an API for AI agents, and community-plugin support.
Quick start Β· Features Β· Configuration Β· Agent API Β· Development Β· Architecture
π Design: PRD.md Β· π Progress: IMPLEMENTATION_PLAN.md
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.
- π Editor & rendering β CodeMirror 6 with live / source / reading views; wikilinks
[[note]], embeds![[file]], tags#tag, callouts, task lists, KaTeX math and Mermaid diagrams. - πΈοΈ Graph view β 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). - π Login gate β a single master password (scrypt-hashed) protects everything; JWT in an httpOnly cookie.
- π Public sharing β turn any note into a read-only, server-rendered (SEO-friendly)
public page at
/share/<token>, 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 β install Obsidian plugins from GitHub; loaded against an Obsidian-API compatibility shim (subset support).
- π± Responsive / mobile β drawer sidebars, edge-swipe, an on-keyboard formatting toolbar, and touch-friendly targets, Γ la Obsidian Mobile.
- ποΈ Pure-JSON config β everything lives in
data/settings.json. No database. - π³ Docker β one command to run the whole stack.
git clone https://github.com/xnohat/webobsidian.git
cd webobsidian
cp .env.example .env # edit VAULT_HOST_PATH, set WEBOBSIDIAN_PASSWORD
docker compose up -d --build
# open http://localhost:8787Out 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.
Prefer a native app? Grab an installer from the Releases page β available for macOS / Windows / Linux (arm64 Β· x64 Β· ia32):
| Platform | Download |
|---|---|
| macOS | .dmg (or .zip) β arm64 / x64 |
| Windows | NSIS installer .exe or portable .exe β x64 / arm64 / ia32 |
| Linux | .AppImage or .deb β x64 / arm64 |
The desktop app bundles the whole server, picks your vault folder on first launch, and
logs you in automatically β no password to type, no Docker. Apps are currently
unsigned, so macOS Gatekeeper / Windows SmartScreen will warn on first open (right-click β
Open on macOS). Build it yourself with npm run desktop:dist; see
desktop/README.md for details.
π Default password is
123456. Log in right away, then change it in Settings β Account. To seed a different password on first run, setWEBOBSIDIAN_PASSWORDin.env. Forgot it? SetWEBOBSIDIAN_PASSWORD(plaintext) orauth.passwordHash(scrypt) as a recovery override.
# .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=8787Then docker compose up -d --build. Your vault can be a plain folder or a git clone
(Git LFS is supported for attachments).
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.
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.confThe 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).
Requires Node β₯ 20 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:5173Production build (the server serves the built SPA):
npm run build
VAULT_PATH=./sample-vault npm start
# open http://localhost:8787Useful 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 workspaces |
| Var | Default | Description |
|---|---|---|
VAULT_HOST_PATH |
./sample-vault |
Host path bind-mounted to /vault |
HTTP_BIND |
0.0.0.0 |
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 |
WEBOBSIDIAN_WATCH |
auto |
auto (native + polling fallback) or polling |
| Var | Default | Description |
|---|---|---|
PORT |
8787 |
HTTP port |
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 |
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.
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.
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/xnohat/webobsidian/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' "<BASE_URL>" "<API_KEY>" > ~/.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 |
Monorepo with two npm workspaces:
webobsidian/
βββ server/ # Express + TypeScript API
β βββ src/{routes,services,middleware,plugins}
βββ web/ # React + Vite SPA (built into server/public)
β βββ src/{components,lib,styles}
βββ data/ # runtime: settings.json + search index (git-ignored)
βββ docs/ # AGENT_API.md, Obsidian internals notes
βββ Dockerfile Β· docker-compose.yml Β· .env.example
βββββββββββββββββββββββββ Browser (React SPA) βββββββββββββββββββββββββ
β CodeMirror 6 Β· Live Preview Β· File Tree Β· Graph Β· Search β
βββββββββββββββββ²βββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββ
β REST + WebSocket β static assets
βββββββββββββββββ΄βββββββββββββββββββββββββββββββββββΌβββββββββββββββββββ
β Server (Node + 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 20+ Β· Express Β· TypeScript Β· React Β· Vite Β· CodeMirror 6 Β· unified/remark/rehype Β· MiniSearch (QMD) Β· simple-git + git-lfs Β· scrypt + JWT Β· Docker.
See PRD.md Β§2 for the full design.
- 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.
- β
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 is a subset of the Obsidian API; plugins relying on Electron/Node internals may not work.
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.
MIT Β© xnohat