Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/webviewjs/webview/llms.txt

Use this file to discover all available pages before exploring further.

Custom protocols let you handle URL schemes such as app:// or api:// directly inside your Node.js process without binding to a network port or running an HTTP server. When the webview navigates to a custom-protocol URL, WebviewJS calls your handler with a standard Fetch API Request object and expects a standard Response in return. Because the interface matches the Web Fetch API, any Fetch-compatible router — including Hono, itty-router, or plain handler functions — works without an adapter. Custom protocols are also the correct approach on Windows, where file: URLs do not support the IPC channel.

Registering a Protocol

Call win.registerProtocol(name, handler) on a BrowserWindow before calling win.createWebview(). Registrations are locked in at webview creation time; adding a scheme after that point has no effect on an existing webview.
import { Application } from '@webviewjs/webview';

const app = new Application();
const win = app.createBrowserWindow({ title: 'My App' });

win.registerProtocol('app', async (request) => {
  return new Response('<h1>Hello</h1>', {
    headers: { 'Content-Type': 'text/html; charset=utf-8' },
  });
});

win.createWebview({ url: 'app://localhost/index.html' });
app.run();
The handler signature is:
type ProtocolHandler = (
  request: Request
) => Response | CustomProtocolResponse | Promise<Response | CustomProtocolResponse>;
Thrown errors and rejected promises are caught automatically and turned into a 500 text/plain response.

Full File-Serving Example

1

Define MIME types

Map file extensions to the correct Content-Type values so browsers parse them properly.
const MIME = {
  '.html': 'text/html; charset=utf-8',
  '.js':   'application/javascript; charset=utf-8',
  '.css':  'text/css',
  '.json': 'application/json; charset=utf-8',
  '.svg':  'image/svg+xml',
  '.png':  'image/png',
  '.woff2':'font/woff2',
};
2

Register the protocol handler

Parse the request URL, resolve the file path, and guard against path traversal before reading from disk.
import { readFile } from 'node:fs/promises';
import { extname, relative, resolve } from 'node:path';
import { Application } from '@webviewjs/webview';

const root = resolve('./dist');

const app = new Application();
const win = app.createBrowserWindow({ title: 'My App' });

win.registerProtocol('app', async (request) => {
  const url = new URL(request.url);
  const pathname = decodeURIComponent(url.pathname).replace(/^\/+/, '') || 'index.html';
  const filePath = resolve(root, pathname);

  // Security: reject any path that escapes the root directory
  if (relative(root, filePath).startsWith('..')) {
    return new Response('Forbidden', {
      status: 403,
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    });
  }

  try {
    const body = await readFile(filePath);
    return new Response(body, {
      headers: {
        'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream',
      },
    });
  } catch {
    return new Response(`Not found: ${url.pathname}`, {
      status: 404,
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    });
  }
});
3

Create the webview and run

win.createWebview({ url: 'app://localhost/index.html' });
app.run();
Always normalize and verify the resolved file path before passing it to the file system. Without the relative(root, filePath).startsWith('..') check, a crafted URL like app://localhost/../../etc/passwd could read arbitrary files from the host machine.

Hono Integration

Because the handler receives a standard Request and returns a standard Response, you can pass router.fetch directly as the protocol handler. No adapter or HTTP server is needed.
import { Hono } from 'hono';
import { Application } from '@webviewjs/webview';

const router = new Hono();

router.get('/', (c) => c.html('<h1>Home</h1>'));
router.get('/about', (c) => c.html('<h1>About</h1>'));

router.get('/api/data', (c) =>
  c.json({ items: ['apple', 'banana', 'cherry'] })
);

const app = new Application();
const win = app.createBrowserWindow({ title: 'Hono App', width: 900, height: 600 });

win.registerProtocol('app', (request) => router.fetch(request));
win.createWebview({ url: 'app://localhost/' });
app.run();
Hono’s middleware, validation, and routing all work exactly as they would in an edge-function deployment — c.req.path, c.req.query(), c.req.json(), etc. are all available.

Legacy CustomProtocolResponse

If you prefer a plain-object response instead of a Response instance, the legacy CustomProtocolResponse shape is still supported:
interface CustomProtocolResponse {
  body: Buffer;           // required — response body bytes
  statusCode?: number;    // default: 200
  mimeType?: string;      // default: application/octet-stream
  headers?: Array<{ key: string; value?: string }>;
}
win.registerProtocol('app', async (request) => {
  const body = await readFile('./index.html');
  return {
    statusCode: 200,
    body,
    mimeType: 'text/html; charset=utf-8',
  };
});
The modern Response-based style is recommended for new code because it is compatible with Fetch-API routers and easier to test in isolation. The legacy object form remains for backward compatibility.

Multiple Protocols Per Window

Register as many schemes as you need before calling createWebview. Common patterns include separating static assets from an API:
// Serve static files from ./dist
win.registerProtocol('app', staticFileHandler);

// Proxy API calls to a remote service
win.registerProtocol('api', async (request) => {
  const url = new URL(request.url);
  const upstream = `https://api.example.com${url.pathname}${url.search}`;
  return fetch(upstream);
});

win.createWebview({ url: 'app://localhost/index.html' });
The page can then fetch api://localhost/users to reach the proxy handler.

CORS and Cache Headers

Add response headers when the page or its scripts require cross-origin access or cache control. This is especially useful for API protocol handlers:
win.registerProtocol('api', async (request) => {
  const data = await queryDatabase(new URL(request.url).pathname);

  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Cache-Control': 'no-store',
    },
  });
});
Once win.createWebview() has been called, the set of registered protocol handlers for that webview is frozen. Calling win.registerProtocol() after createWebview() will not affect the already-created webview instance. Always register all schemes before creating the webview.
// ✅ Correct order
win.registerProtocol('app', handler);
win.registerProtocol('api', apiHandler);
const webview = win.createWebview({ url: 'app://localhost/' });

// ❌ Too late — this registration is ignored by the webview above
win.registerProtocol('assets', assetsHandler);

Build docs developers (and LLMs) love