ffwebapps — Architecture Overview

This page is the system map for ffwebapps: the binaries, the shared library they all call, the Firefox runtime they drive, and the single socket that ties a running app to its tray and launcher.

Table of Contents

  1. What ffwebapps is
  2. The core idea: drive, don’t patch
  3. Component model
  4. The shared library
  5. On-disk state
  6. Lifecycle of an app
  7. The one IPC boundary
  8. Source layout
  9. Design rules
  10. Document index

1. What ffwebapps is

ffwebapps runs any website as a native, chromeless desktop app on Linux: its own window with no tabs or address bar, its own taskbar/dock identity, a system-tray icon with an unread badge, close-to-tray, and out-of-scope links that open in your real browser. It is a CLI-driven fork of PWAsForFirefox’s native component, re-architected to drive Firefox’s first-party Web Apps (Taskbar Tabs) feature rather than patch the browser chrome at runtime.

The whole project is one Rust crate (firefoxpwa) that produces several binaries plus a small bundle of privileged Firefox configuration. There is no long-lived ffwebapps daemon: the running app is a Firefox process, and everything else is either a one-shot CLI command or a thin client of the socket that Firefox process serves.

ffwebapps component architecture control surfaces -- link the library in-process no shelling out, no CLI-text parsing: each constructs command structs and calls .run() ffwebapps (CLI) ffwebapps-gtk (GTK4 / libadwaita GUI) firefoxpwa-connector inherited native-messaging host (unused by CLI flow) firefoxpwa -- the shared library (one crate, called in-process) manifest fetch, system integration, storage; the only code that mutates app state storage / components Site / Profile / Runtime / Config over config.json taskbartabs writes taskbartabs.json + per-app user.js prefs integrations .desktop launchers, icons, autostart, KWin rules console (clap commands) site / profile / runtime; launch builds Firefox argv read / write on-disk state (~/.local/share/ffwebapps) config.json sites + profiles profiles/<ulid>/ Firefox profile runtime/ linked Firefox desktop integration (XDG dirs) applications/ FFPWA-<ulid>.desktop icons/hicolor/ app icons autostart/ launch-on-login firefox -profile … -taskbar-tab <webapp_id> -- launch boundary spawns the running app -- a system Firefox process (Web App window) owns its window and lifecycle; configured entirely by files in its profile _autoconfig.cfg (privileged JS) serves the Unix socket, owns hide/show, routes out-of-scope links to the browser, injects per-app CSS / JS, tracks unread profile config (read at launch) user.js -- link allow-list, perf prefs, UA chrome/userChrome.css -- chromeless titlebar taskbartabs/taskbartabs.json -- scope window identity app_id org.mozilla.firefox.webapp-<id> MOZ_APP_REMOTINGNAME ffwebapps-<ulid> = distinct dock entry + single instance serves $XDG_RUNTIME_DIR/ffwebapps-<ULID>.sock -- the one IPC boundary (newline protocol) runtime alive iff socket accepts; no pidfiles, no sentinels ffwebapps-tray StatusNotifierItem: icon, badge, menu launcher (site launch) connect succeeds = focus, don't duplicate ffwebapps-gtk live control running / hidden / unread + show / hide / quit

2. The core idea: drive, don’t patch

The upstream project, and most “site-specific browser” tools, achieve a chromeless window by patching the browser’s chrome at runtime — replacing browser.xhtml, injecting a custom UI, and maintaining that patch against every Firefox update. ffwebapps takes the opposite stance: it leans on a feature Mozilla now ships and maintains itself.

Firefox ≥ 151 includes Web Apps (Taskbar Tabs): pass firefox -taskbar-tab <id> and Firefox opens a standalone, minimal-UI window with its own Wayland app_id. ffwebapps' job is reduced to three things:

  1. Register the app in the profile’s taskbartabs.json so the ID resolves to a scope and a start URL.
  2. Configure the runtime through first-party mechanisms — a privileged _autoconfig.cfg, a profile user.js, and userChrome.css — to enable the feature, strip the last toolbar pixels, route links, and serve a tray socket.
  3. Integrate with the desktop — generate the .desktop launcher, icons, and (on KDE) a window rule.

Nothing monkeypatches Firefox’s UI code. The chromeless look is userChrome.css; the behaviour is _autoconfig.cfg. Both are supported, documented Firefox extension points. See The Firefox Runtime & Autoconfig for the full mechanism.

3. Component model

ffwebapps is built from four binaries and one configuration bundle. Only the first three binaries matter for the Linux web-app flow; the connector is inherited from upstream.

Component Kind Role
ffwebapps CLI binary The primary interface: install / launch / update / uninstall apps, profiles, and the runtime
ffwebapps-tray Tray binary A StatusNotifierItem that shows the icon, unread badge, and menu, and drives the window over the socket
ffwebapps-gtk GUI binary A GTK4 / libadwaita management app behind the gui cargo feature; same crate, calls the library in-process
firefoxpwa-connector Helper binary Native-messaging host inherited from PWAsForFirefox; not part of the CLI-driven flow
userchrome/ Config bundle _autoconfig.cfg, autoconfig.js, and userChrome.css installed into the runtime / profile

The crucial property: there is no ffwebapps daemon. A web app that is “running” is a Firefox process. The CLI and GUI are short-lived; the tray is a thin remote that exits when its app does. State lives on disk and in that one Firefox process.

4. The shared library

Everything except the privileged JS lives in the firefoxpwa crate, and src/lib.rs re-exports each module so every binary calls the same code in-process:


pub mod components;   // Site, Profile, Runtime, taskbartabs registry
pub mod connector;    // inherited native-messaging protocol
pub mod console;      // clap command structs + the Run trait
pub mod directories;  // ProjectDirs — the on-disk layout
pub mod integrations; // .desktop / icons / autostart / KWin rules
pub mod storage;      // Storage + Config over config.json
pub mod utils;

The GTK GUI was deliberately built as a second binary in the same crate rather than a separate program. The core types (Site, Config, Storage, ProjectDirs, Profile) are #[non_exhaustive], which forbids construction from other crates but not from within this one — so the GUI can build command structs and call .run() directly, reusing manifest fetching, system integration, and storage writes instead of re-implementing or shelling out. See Data Model & Storage and CLI & Command Model.

5. On-disk state

A web app is fully described by files. There is no hidden runtime database; deleting the directories below removes the app.

Path Holds
~/.local/share/ffwebapps/config.json The Storage: every Site, every Profile, and the global Config
~/.local/share/ffwebapps/profiles/<profile-ulid>/ The Firefox profile for that profile’s apps (user.js, chrome/, taskbartabs/, ffwebapps.css/js)
~/.local/share/ffwebapps/runtime/ The runtime — symlinks to the system Firefox when installed with --link
~/.local/share/applications/FFPWA-<ulid>.desktop The launcher that appears in the app menu
~/.local/share/icons/hicolor/<size>/apps/FFPWA-<ulid>.png Rasterised app icons
~/.config/autostart/FFPWA-<ulid>.desktop Present only when launch on login is enabled
$XDG_RUNTIME_DIR/ffwebapps-<ULID>.sock The live IPC socket — exists only while the app runs

Profiles are the unit of isolation: several apps can share one Firefox profile (and thus cookies, storage, and per-profile CSS/JS), or each can have its own. The Nil-ULID profile is the shared “Default”.

6. Lifecycle of an app

A single site install followed by a launch touches most of the system. The sequence makes the boundaries concrete:

  1. Install (ffwebapps site install <MANIFEST_URL> --document-url <PAGE>): the library fetches the web-app manifest and icons, mints a ULID and a webapp_id (a UUID), writes the Site into config.json, generates the .desktop launcher and icons, and — on KDE — a kwinrulesrc position rule.
  2. Launch (from the menu or site launch <ULID>): the console first probes the socket. If it connects, the app is already running, so it sends show and exits — the single-instance guarantee. Otherwise it writes taskbartabs.json and user.js, builds the Firefox argv, and spawns the runtime, optionally under a scheduling policy.
  3. Run: Firefox loads _autoconfig.cfg, which binds the socket, takes ownership of the window’s hide/show, starts routing out-of-scope links, injects any per-app CSS/JS, and begins exporting the unread count.
  4. Tray: the runtime (via the launch path) spawns ffwebapps-tray, which connects to the socket, draws the icon, and relays menu actions back as verbs.
  5. Close to tray: the window’s X is intercepted and turned into a hide while a tray client is connected; otherwise it really closes. Quit (from the tray menu) sends quit, the runtime tears the socket down, and the tray sees EOF and exits.

7. The one IPC boundary

The entire live surface is a single Unix-domain socket, served by the Firefox runtime and consumed by thin clients. It uses a newline-delimited text protocol:


client -> runtime : hello v1 tray | hello v1 launcher
                    show | hide | toggle | quit | reload
                    mute-toggle | dnd-toggle | suspend-toggle
                    autostart-toggle | copy-url | open-browser
runtime -> client : hello v1 <pid>            (once, on connect)
                    unread <n>                (on change)
                    state hidden=… muted=… dnd=… suspend=… autostart=…

There are no pidfiles and no sentinel files: the runtime is alive if and only if the socket accepts a connection. That single invariant powers the singleton launcher, the tray’s liveness detection, and the GUI’s running/hidden status dot. The full protocol, the runtime-owned-window model, and the KWin hide/show mechanism are documented in IPC & the Runtime-Owned Window.

8. Source layout

Path Purpose
src/bin/ffwebapps.rs CLI entrypoint — App::parse().run()
src/bin/ffwebapps-tray.rs The ksni tray helper
src/bin/ffwebapps-gtk/ The GTK4 management GUI (its own core / ipc / ui modules)
src/components/ Site, Profile, Runtime, and the taskbartabs registry writer
src/console/ clap command structs (app, profile, runtime) and the Run trait
src/integrations/ Desktop integration; implementation/linux.rs is the Linux backend
src/storage.rs, src/directories.rs Storage / Config and the on-disk path resolver
userchrome/runtime/_autoconfig.cfg The privileged runtime JS — the heart of the running app
userchrome/profile/chrome/userChrome.css The chromeless titlebar styling

9. Design rules

The architecture holds together because of a few deliberate choices, several of them learned the hard way (see the gotchas in each page):

10. Document index

Document Covers
The Firefox Runtime & Autoconfig Driving Web Apps / Taskbar Tabs, the privileged _autoconfig.cfg, userChrome.css, and profile prefs
IPC & the Runtime-Owned Window The socket protocol, close-to-tray, and the KWin hide/show mechanism
Data Model & Storage Storage, Config, Site, Profile, Runtime, ULIDs, and the on-disk layout
CLI & Command Model The clap command tree, the Run trait, and the update-value semantics
System Tray The ksni StatusNotifierItem, the flock singleton, and the menu
GTK Management GUI The ffwebapps-gtk app: pages, off-thread workers, and live control
Link Routing & Scope Two-layer out-of-scope interception and the in-app allow-list
Desktop Integration .desktop launchers, icons, autostart, window identity, and KWin rules
Performance Tuning --hardware-webrtc, --scheduling, --software-rendering, and memory
Build, Install & Packaging Building, the runtime link step, and packaging