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.
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.
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:
taskbartabs.json so the ID resolves to a scope and a start URL._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..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.
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.
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.
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”.
A single site install followed by a launch touches most of the system. The sequence makes the boundaries concrete:
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.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._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.ffwebapps-tray, which connects to the socket, draws the icon, and relays menu actions back as verbs.quit, the runtime tears the socket down, and the tray sees EOF and exits.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.
| 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 |
The architecture holds together because of a few deliberate choices, several of them learned the hard way (see the gotchas in each page):
Command::new calls — so it can never launch, relaunch, or strand a process.user.js and registry are managed by the library and regenerated at launch; nothing is hidden in opaque daemon state.| 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 |