Browser Compatibility Layer for Obsidian Desktop

Run the official Obsidian app in your browser.

Nothing from Obsidian is bundled or redistributed. Your browser loads the official obsidian-x.y.z.asar.gz release straight from Obsidian's GitHub releases, verifies its published SHA-256 hash, and runs it locally over a Service Worker and Electron/Node.js shim.

Notes stay on your device — in browser IndexedDB or mounted directly to a local disk folder (Chromium).
Architecture

How It Works Under the Hood

Obsidian's desktop build is an Electron application packaged inside an .asar archive that expects Node.js modules, synchronous disk access, and Electron IPC. obsidian4web bridges those expectations directly inside a standard browser context.

1. Client-Side Release Verification & Caching

No proprietary Obsidian code is hosted by this project. On first run, the launcher checks Obsidian's official release metadata (desktop-releases.json) and acquires the unmodified obsidian-x.y.z.asar.gz archive — either fetched automatically in one click through a self-hosted proxy, or dropped in by the user on static hosts like GitHub Pages (since GitHub release downloads block cross-origin requests). The browser verifies the archive's SHA-256 digest using Web Crypto, decompresses it via DecompressionStream, indexes its header, and persists it in Cache Storage for instant offline startup.

2. Service Worker Protocol Emulation

In desktop Electron, Obsidian loads its application bundle from a custom app:// protocol and embeds vault media via local resource URLs. In the browser, a Service Worker intercepts requests within the app scope, slices requested files straight out of the cached .asar blob by byte offset (including HTTP byte-range support), rewrites HTML entry points to inject the compatibility runtime before Obsidian's scripts execute, and streams vault attachments on demand from the active tab.

3. Synchronous VFS & Electron API Shim

Obsidian's core and community plugins rely heavily on synchronous Node.js fs calls (readFileSync, statSync, readdirSync) alongside Electron's ipcRenderer and @electron/remote. The runtime maintains an in-memory POSIX file tree that satisfies reads and writes synchronously while flushing changes asynchronously to IndexedDB or directly to a local folder on disk through Chromium's File System Access API. Synchronous native file pickers, application menus, popout windows, and sandboxed <webview> frames are emulated in the DOM.

4. Optional CORS & WebSocket Relay

Everything that runs offline works on pure static hosting. For features that contact external servers without browser CORS headers — one-click app downloads/updates, the Community Plugin & Theme store, Obsidian Sync & Publish, and WebDAV/plugin APIs — the companion server relays HTTP requests and WebSocket streams with desktop-compatible headers while enforcing same-origin session cookies, destination allowlists, and DNS-rebinding SSRF protections.

Deployment Modes

GitHub Pages (No Proxy) vs. Self-Hosted (With Proxy)

The static build in ./app/ works on any static host without a backend. Running the prebuilt GHCR container unlocks cross-origin requests that browsers otherwise block via CORS.

Capability GitHub Pages / Static (No Proxy) Self-Hosted (With Proxy)
Core editor, Markdown, Canvas, Graph & core plugins Full support Full support
Vault storage (Browser IndexedDB & local disk folders on Chromium) Full support Full support
Offline launch after initial setup Full support (Cache Storage + SW) Full support (Cache Storage + SW)
App installation & updates Manual file drop Drop official obsidian-x.y.z.asar.gz (SHA-256 verified) One-click automatic Downloads & updates in-browser
Community plugins & themes Manual copy Copy into .obsidian/plugins/<id>/ of a disk-mounted vault In-app store install Browse & install directly in settings
Obsidian Sync & Obsidian Publish Unavailable Blocked by CORS on Obsidian's API Supported Proxies HTTPS & Sync WebSockets
Plugin network calls & WebDAV / self-hosted sync CORS-enabled APIs only Supported Allowlist extra hosts via O4W_PROXY_ALLOW_HOSTS
Web viewer core plugin Direct iframe Works for sites that permit framing Full proxy + Reader mode When O4W_PROXY_WEBVIEWER=1 or open mode
Self-Hosting Guide

Deploy with the Prebuilt GHCR Image

Multi-arch images (linux/amd64, linux/arm64) are published to GitHub Container Registry at ghcr.io/d-513/obsidian4web:latest. Self-hosting requires HTTPS (browsers require a secure context for Service Workers, except on localhost). Any number of hostnames can point to the same instance — each hostname is a separate browser origin with its own vaults.

1. Docker Run (GHCR — No Build Needed)
docker run -d --name obsidian4web \
  --restart unless-stopped \
  -p 127.0.0.1:8080:8080 \
  -e O4W_SECRET="$(openssl rand -hex 32)" \
  -e O4W_TRUST_PROXY=1 \
  -e O4W_PROXY_MODE=allowlist \
  ghcr.io/d-513/obsidian4web:latest
Pulls the prebuilt multi-arch image from GHCR and runs as non-root user node with a built-in /healthz check. No Obsidian binaries are inside the image.
2. Docker Compose (docker-compose.yml)
services:
  obsidian4web:
    image: ghcr.io/d-513/obsidian4web:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      O4W_SECRET: ${O4W_SECRET:?set O4W_SECRET to a long random string}
      O4W_TRUST_PROXY: "1"
      O4W_PROXY_MODE: allowlist
      # O4W_PROXY_ALLOW_HOSTS: ".openai.com,api.anthropic.com"
      # O4W_PROXY_WEBVIEWER: "1"
Start with O4W_SECRET="$(openssl rand -hex 32)" docker compose up -d.
3. Reverse Proxy — Caddyfile (TLS + WebSockets)
# deploy/Caddyfile — run container with O4W_TRUST_PROXY=1
notes.example.com, obsidian.example.org {
    encode zstd gzip
    reverse_proxy 127.0.0.1:8080 {
        header_up X-Forwarded-Host {host}
        header_up X-Forwarded-Proto {scheme}
    }
}
Pass X-Forwarded-Host and X-Forwarded-Proto and set O4W_TRUST_PROXY=1 so same-origin checks work behind TLS termination.
4. Reverse Proxy — nginx.conf (TLS + WebSockets)
map $http_upgrade $connection_upgrade { default upgrade; '' close; }

server {
    listen 443 ssl http2;
    server_name notes.example.com;
    client_max_body_size 64m;
    proxy_read_timeout 3600s;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_buffering off;
    }
}
proxy_read_timeout 3600s keeps Obsidian Sync WebSockets alive; proxy_buffering off streams release downloads smoothly.
Local Development / Running Directly with Node.js (>= 20.18 + pnpm)
pnpm install
pnpm dev                       # http://localhost:5173 with proxy (allowlist mode)
O4W_PROXY_MODE=open pnpm dev   # allow any public host (enables Web viewer pages)
pnpm test                      # run proxy hardening test suite
Server Reference

Proxy Configuration & Hardening

Configure the ghcr.io/d-513/obsidian4web container with environment variables. By default the proxy is locked down to Obsidian's services and GitHub only.

Variable Default Description
O4W_SECRET random per start HMAC key for signing session cookies. Set a fixed random secret in production.
O4W_PROXY_MODE allowlist allowlist: Obsidian services & GitHub only. open: any public host. off: disable proxy.
O4W_PROXY_ALLOW_HOSTS empty Comma-separated extra hosts allowed in allowlist mode (e.g. .openai.com,api.anthropic.com; leading dot matches subdomains).
O4W_PROXY_WEBVIEWER on in open Allow proxied Web viewer pages for any public host (GET, sandboxed). Without it, sites are framed directly.
O4W_TRUST_PROXY 0 Honour X-Forwarded-Host, X-Forwarded-Proto, and X-Forwarded-For. Only enable behind a reverse proxy.
O4W_ALLOWED_ORIGINS empty Extra origins permitted to use the proxy in addition to each request's own host.
O4W_RATE_LIMIT_RPM 300 Maximum proxied requests per minute, per IP and per session.
O4W_RATE_LIMIT_MB_PER_HOUR 2048 Transfer quota (MB/hour) per IP and per session.
O4W_MAX_CONCURRENT 24 Maximum concurrent proxied requests per IP and per session.
O4W_MAX_REQUEST_MB / O4W_MAX_RESPONSE_MB 64 / 200 Request and upstream response body size caps in megabytes.
O4W_TIMEOUT_S 120 Upstream request timeout in seconds.
O4W_MAX_WEBSOCKETS 8 Maximum concurrent open WebSockets (Sync) per IP and per session.
O4W_TOKEN_TTL_HOURS 24 Session cookie lifetime in hours (automatically renewed while active).
O4W_PROXY_ALLOW_PRIVATE 0 Allow private/loopback IP destinations (only for trusted self-hosted LAN setups).
O4W_PROXY_LOG 0 Log proxied requests to stdout when set to 1.

Why the Proxy Is Not an Open Relay

  • Strict same-origin caller verification: Requests must carry matching Origin / Sec-Fetch-Site: same-origin, a custom X-O4W-Proxy header (which browsers cannot send cross-site without a CORS preflight that the server never grants), and a signed HttpOnly; SameSite=Strict session cookie.
  • SSRF & DNS-rebinding protection: Private and loopback IPv4/IPv6 ranges are rejected by default, DNS answers are verified at socket connect time, and every HTTP redirect hop is re-evaluated against the policy.
  • Opaque sandboxed responses: Proxied pages carry Content-Security-Policy: sandbox so upstream content never executes in the site's origin, and cookies are never forwarded in either direction.
Good to Know

Storage & Known Limitations

Vault Storage Options

  • Browser-stored vaults: Persisted locally in your browser's IndexedDB and work across all modern browsers (Chrome, Firefox, Safari, Edge).
  • Local disk folders (Chromium): Mounted directly from your computer's filesystem via the File System Access API — edits write straight to disk and external file changes are picked up live.
  • Offline application cache: Once the verified Obsidian .asar release is installed into browser Cache Storage, the entire app boots offline without re-downloading.
  • Origin isolation: Each hostname serving obsidian4web has its own isolated browser storage, vaults, and settings.

Known Browser Constraints

  • Browser-reserved shortcuts: Ctrl/Cmd+N, T, and W are reserved by tabbed browsers. Installing the site as a PWA ("Install app") restores them.
  • Local disk folders: Require a Chromium-based browser (Chrome, Edge, Brave, Arc). Firefox and Safari use IndexedDB vaults.
  • Popout windows: Are not automatically restored across page reloads because browsers block popups not triggered by a user click.
  • Export to PDF & native plugins: PDF export opens the browser Print dialog (Save as PDF). Plugins requiring native Node modules or child_process cannot run in a browser.