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