Murasaki
Core Concepts

Process model

Startup, development reloads, graceful shutdown, and app-owned resources.

Murasaki separates the native event loop from Node's event loop. In a packaged application the Rust launcher starts and supervises the bundled Node child; the Node child starts your Main lifecycle and local HTTP server; only then does the host load the renderer.

launch
  → Rust host acquires the per-user app lock
  → host chooses the app-local origin and creates a runtime token
  → Node child loads src/main.ts
  → main.ready(context) finishes
  → local server begins listening
  → registered cold-start URLs/files are delivered to main.openRequested()
  → declared WebViews load their renderer routes

If ready() rejects, startup fails instead of showing a renderer attached to a half-initialized backend. Use it for resources that must exist before the UI is usable: opening a database, running migrations, or starting an app-owned service.

The primary process receives its bounded raw arguments and working directory as MainContext.launch. During development, only arguments after the standalone delimiter in murasaki dev -- <app-arguments> enter that snapshot; Murasaki's CLI flags are never forwarded. See Node Main.

In packaged macOS, Windows, and Linux apps, a second launch does not start another backend. Its argv / working directory are delivered to main.secondInstance(), and the host focuses the primary window. Development mode does not currently provide this lock.

Open requests

Packaged macOS and Windows apps, plus installed Linux .deb packages, can register custom URL schemes and document extensions with protocols and fileAssociations in murasaki.config.ts. After ready() has completed, matching activations are normalized and delivered to main.openRequested():

  • activation is cold-start, second-instance, or os-event.
  • transport is argv, open-url, or open-file.
  • targets contains { kind: 'url', url, scheme } or { kind: 'file', path } values.

On a second launch, secondInstance() still receives the raw process arguments while openRequested() receives only registered URLs and files. Use openRequested() for cross-platform open behavior and secondInstance() for other secondary-launch arguments.

On Linux, the .deb installs the generated .desktop entry. A manually extracted AppDir or AppImage contains the same metadata but does not register it with the desktop environment automatically.

URL and file activations are untrusted operating-system input. Registration selects which inputs reach the hook; it does not authenticate the sender or make the URL contents or file contents safe.

Quit sequence

Normal window-close and app-quit requests use the lifecycle below:

quit request
  → beforeQuit(context)       may return false ┐
  → context.signal aborts
  → shutdown(context)                             ┘ bounded together by shutdownTimeoutMs
  → Node child exits
  → native host exits or applies an update

beforeQuit() may cancel a normal close—for example while a document has unsaved changes. Forced shutdown paths, including process signals and dev Main reloads, ignore cancellation. The combined hooks default to a 10-second limit; shutdown() should stop accepting work first, then flush and close resources.

The OS close control and appWindow.close() hide a secondary, so it can be shown again with windows.open(). Closing the primary main window is an application quit request and follows the sequence above. Cancellation is application-wide; Murasaki does not expose a cancellable per-window close hook. Node Main windows.destroy(label) releases a secondary target, and windows.create(label) can recreate its configured declaration.

Development reloads

During murasaki dev, a change to the configured Main entry triggers:

  1. the current shutdown({ reason: 'dev-reload' }),
  2. Vite invalidation of that module,
  3. construction of a fresh lifecycle,
  4. a new ready() call.

Changes imported by Main still follow Vite's module graph, but only the exact configured entry currently triggers the lifecycle restart. Avoid module-level timers or sockets: create them inside ready() and close them inside shutdown() so reloads do not leak work.

Application paths

MainContext.paths gives stable, per-appId locations:

PathIntended data
dataDatabases, user documents owned by the app, durable state
cacheRe-creatable caches
logsApplication logs
tempEphemeral staging files

Use these instead of writing beside the executable. Packaged resources may be read-only, app locations vary by OS, and updates may replace the application bundle.

Process failures

The host owns and polls the Node child lifetime. If the packaged backend exits unexpectedly, the launcher discards any unconfirmed update handoff, closes its WebViews, terminates the backend process tree, and exits non-zero rather than leaving a dead UI. Windows uses a Job Object; macOS launches Node and its sidecars in a dedicated process group. Murasaki does not yet expose a public crash-restart policy, health-check hook, or multiple independently supervised worker groups. Use the Main sidecar supervisor when a helper needs an explicit restart policy.

Next

Improve this page on GitHub

On this page