Chapter 50 / 51
Volume 5 · Extensions, Ops & Projects

webview and the app node: turning a site into a desktop app

Everything the previous forty-nine chapters produced was a "site" — open a browser, type an address. This chapter gives it a window: a class: "app" service node = a local HTTP service + a WebView2 desktop window. Configuration is all it takes, degradation is automatic, and combined with xs pack you get a double-click single-exe application.

Overview

Positioning: the desktop-form finale (home turf of the 15th extension, webview). The core mental model in one sentence — the window is the display layer, the service is the substance: the app node is not a sixth protocol but the frontend-ization of an http node. host_default / hosts / static / TLS / hot reload all mean the same; only a window descriptor and a frontend lifecycle are added.

50.1 Your first desktop app

Take any existing site's service node, change it to class: "app" and add a window descriptor:

Config example · app node
{
  "enabled": true, "class": "app", "name": "main",
  "ip": "127.0.0.1", "port": 0,
  "host_default": { "name": "app", "path": "wwwroot", "devlang": "c",
                    "devfile": "script/http_main.c" },
  "window": {
    "title": "My App", "width": 1280, "height": 800,
    "url": "/", "devtools": false,
    "close": "stop", "install_fallback": "ask"
  }
}

Start xs: a loading window appears first (a placeholder before the service is ready); once the local HTTP service is up, the main interface at url opens automatically. Note the two defaults that differ from the http class: ip defaults to 127.0.0.1 (loopback binding does not trigger the Windows firewall popup) and port allows 0 (auto-assign, see 50.3). Field-by-field constraints for the nine window fields (including min_width / min_height and the install_fallback value table) are in the xs.json field reference.

The build variant must compile in the webview extension: build.bat webview (or all); a variant without the extension still parses the config — there is just no window (see the matrix in 50.4).

50.2 Lifecycle & close semantics

  • Startup: the loading window comes first (covering the service-initialization gap) → service ready → automatic navigation to the main interface
  • close: "stop" (default): the user closing the window = graceful shutdown through the same chain as Ctrl+C — not a process kill
  • close: "hide": closing only hides; the service keeps running. Launching the same app again wakes the existing window via single-instance detection (a named mutex derived from the exe path) and the new process exits immediately. v1 has no tray icon; in hide mode shutdown goes through the signal path
  • Crash isolation: the WebView2 renderer is a separate process — a page crash does not infect xs; any window-thread failure degrades to a log line and never pushes the engine assembly onto the fail path

50.3 port: 0 auto-assignment

A desktop app's port should be invisible to the user and should not fight other applications — port: 0 hands allocation to the OS atomically (allowed only for the app class; other classes fail fast):

  • Readback: after a successful bind the actual port lands in XS_ServerInfo.PortBound (Port is written back too, so endpoint identity matches the script view); the frontend builds its URL from it
  • From a script: xsServerFind("main")->PortBound, rendered into the page so the frontend knows who it is (e.g. the WS address)
  • Hot-reload stickiness: a sticky table keyed by service name (surviving generations) rebinds the old port explicitly on swap — open windows never notice; if the sticky port is taken, the candidate fails and cleans up per the existing failure path
  • Boundary: the sticky table is not persisted. For a port stable across restarts, configure a fixed port (especially when external systems call you back)

50.4 Degradation matrix & release

Build variantPlatformWebView2Behavior
webview extensionWindowsinstalledfull window functionality
webview extensionWindowsmissingper install_fallback: ask prompts / silent installs / headless degrades at once; any failure lands on headless
webview extensionLinux—headless, the access URL printed in the log
no extensionany—same headless plus one notice line; config parses as usual

The release combo: xs pack (chapter 48) packs the site into the binary + xsw.exe (the no-console GUI-subsystem variant shipped with the release package) — the artifact is a double-click single-exe desktop app: loading window → interface → close-to-stop, no black console at any point. Zero runtime DLLs: the import table lists system DLLs only; WebView2Loader.dll is located at runtime from the installed Runtime directory, with install_fallback covering a missing Runtime.

Common mistakes

Pit: the port changed after a restartport:0 stickiness lives only while the process lives (the hot-reload scenario); across restarts every launch gets a fresh assignment. When external systems must call you back or users bookmark the address, configure a fixed port.
Pit: manually switching ip to 0.0.0.0 triggers the firewall popupThe app class defaults to 127.0.0.1 precisely so loopback binding skips the Windows firewall authorization dialog. Don't change it back when the desktop app doesn't need to listen publicly.
Pit: no window on LinuxThat's by design, not a bug — webview's window backend is Windows-only; Linux degrades to headless automatically. Take the address from the log's open http://… in a browser line. The same xs.json stays portable across platforms.
Pit: shipping with devtools onwindow.devtools defaults to false; handy during development, close it before release — a user pressing F12 sees your interfaces and data shapes.

Exercises

Basic
Put the Volume-2 companion static site into an app-node window and try close: stop then close: hide (including the second-launch re-wake).
Intermediate
Start with port: 0, read PortBound from a script via xsServerFind and render it into the page (e.g. the WS connect address).
Challenge
xs pack a single exe, copy it to a machine without the WebView2 Runtime and verify the install_fallback: "ask" prompt, then switch to "headless" and verify direct degradation.

Summary

class: "app" = frontend-ized http (same config + 9 window fields) · loading → ready → open URL → close-to-stop = the lifecycle · port:0 + PortBound = the invisible port (fixed for cross-restart stability) · the degradation matrix = window best-effort, service first · xs pack + xsw.exe = the double-click desktop exe