Skip to content

JavaScript Bridge preview

The Hermes interop bridge provides bidirectional communication between your JavaScript frontend and C# backend. It works with any framework — React, Vue, Svelte, Angular, or vanilla JS.

Overview

The bridge uses a JSON-based protocol over the native WebView message channel. From JavaScript, you call C# methods and receive results as promises. From C#, you can push events to the frontend. No HTTP server, no WebSocket — messages flow directly through the WebView's native messaging API.

Installation

Install the bridge package in your frontend project:

bash
npm install @hermes/bridge

Invoking C# Methods

Registering Handlers (C#)

Register methods in your Program.cs using UseInteropBridge:

csharp
builder.UseInteropBridge(bridge =>
{
    // No arguments, returns a value
    bridge.Register("getRuntime", () => $".NET {Environment.Version}");

    // One argument, returns a value
    bridge.Register<string, string>("greet", name => $"Hello, {name}!");

    // Async handler
    bridge.RegisterAsync<string, string>("fetchData", async (query) =>
    {
        var result = await _dataService.SearchAsync(query);
        return result;
    });
});

Calling from JavaScript

Use bridge.invoke() to call registered C# methods. It returns a Promise that resolves with the result or rejects on error:

typescript
import { bridge } from '@hermes/bridge';

// Simple call
const runtime = await bridge.invoke<string>('getRuntime');

// With arguments
const greeting = await bridge.invoke<string>('greet', 'World');

// With timeout
const data = await bridge.invoke<SearchResult>(
  'fetchData',
  { timeout: 5000 },
  'search query'
);

Error Handling

If the C# handler throws an exception, the promise rejects with an Error containing the exception message:

typescript
try {
  const result = await bridge.invoke<string>('riskyMethod');
} catch (err) {
  console.error('C# method failed:', err.message);
}

Events

Events provide fire-and-forget messaging in both directions.

JavaScript to C# Events

Send events from JavaScript:

typescript
bridge.send('user-action', { action: 'clicked', target: 'save-button' });

Handle them in C#:

csharp
builder.UseInteropBridge(bridge =>
{
    bridge.On<UserAction>("user-action", action =>
    {
        Console.WriteLine($"User clicked: {action.Target}");
    });
});

C# to JavaScript Events

Listen for events in JavaScript:

typescript
const unsubscribe = bridge.on<ProgressData>('download-progress', (data) => {
  console.log(`Progress: ${data.percent}%`);
});

// Clean up when done
unsubscribe();

Environment Detection

Check if your code is running inside a Hermes desktop window:

typescript
if (bridge.isHermes) {
  // Running in Hermes — native features available
  const runtime = await bridge.invoke<string>('getRuntime');
} else {
  // Running in a browser — fall back gracefully
}

This is useful for apps that need to work both as a desktop app and in a browser.

Protocol Reference

The bridge uses JSON envelopes over the WebView's native window.external channel:

DirectionTypeShape
JS → C#Invoke{"type":"invoke","id":"...","method":"greet","args":["World"]}
C# → JSResult{"type":"result","id":"...","value":"Hello, World!"}
C# → JSError{"type":"error","id":"...","message":"..."}
EitherEvent{"type":"event","name":"...","data":{...}}

Each invoke gets a unique id, and the corresponding result or error carries the same id so the bridge can match responses to promises.

Registration API Reference

C# MethodSignatureDescription
Register(string, Func<object?>)No-arg handler returning a value
Register<TResult>(string, Func<TResult>)Typed no-arg handler
Register<TArg, TResult>(string, Func<TArg, TResult>)Single-arg typed handler
RegisterAsync<TResult>(string, Func<Task<TResult>>)Async no-arg handler
RegisterAsync<TArg, TResult>(string, Func<TArg, Task<TResult>>)Async single-arg handler
On(string, Action)Event listener (no data)
On<T>(string, Action<T>)Typed event listener

Next Steps