This page documents the running app itself: how ffwebapps drives Firefox’s first-party Web Apps feature, and the privileged _autoconfig.cfg JavaScript that turns a plain Firefox into a chromeless, socket-served, link-routing web-app runtime.
Firefox ≥ 151 ships a first-party Web Apps (Taskbar Tabs) feature. Launch it with firefox -taskbar-tab <id> and Firefox opens a standalone, minimal-UI window whose Wayland app_id is org.mozilla.firefox.webapp-<id> — a window the desktop treats as its own application. ffwebapps' entire approach is to enable and configure that feature rather than replace Firefox’s UI.
The contrast with the usual site-specific-browser approach is the whole point. Instead of overwriting browser.xhtml and re-applying that patch against every Firefox release, ffwebapps uses three supported extension points, each owned by a different file:
| Mechanism | File | Responsibility |
|---|---|---|
| Autoconfig (enterprise policy JS) | _autoconfig.cfg |
Enable the feature; serve the tray socket; own hide/show; route links; inject CSS/JS |
Profile user.js |
<profile>/user.js |
Per-app prefs: link allow-list, performance knobs, UA override |
userChrome.css |
<profile>/chrome/userChrome.css |
Strip the last toolbar pixels for a chromeless titlebar |
Nothing here is a private API or a binary patch. Autoconfig is the mechanism enterprises use to lock down Firefox; user.js and userChrome.css are documented customization hooks.
The “runtime” is a Firefox install that ffwebapps owns under ~/.local/share/ffwebapps/runtime/. On Linux the preferred mode is --link, which avoids a second copy of Firefox entirely (Runtime::link, runtime.rs:344-399):
defaults/ → a defaults/pref/ directory is created and channel-prefs.js is symlinked.firefox and firefox-bin → copied as real files (a symlinked main binary misbehaves)./usr/lib/firefox/ → symlinked into the runtime directory.The result is a Firefox that tracks the system package’s updates but lives in ffwebapps' directory, where it can carry the autoconfig. The alternative, runtime install without --link, downloads an official Mozilla build and unpacks it instead. Config::use_linked_runtime records which mode is active.
Runtime::patch (runtime.rs:410-496) copies sysdata/userchrome/runtime/ into the runtime directory and (on Linux) chmods the files to 0o644. That copied payload is what makes the Firefox an ffwebapps runtime: the autoconfig loader and the autoconfig itself.
Firefox’s autoconfig is a two-file handshake. The small autoconfig.js is a normal pref file that lives in defaults/pref/ and tells Firefox which autoconfig to load and how to read it:
pref('general.config.filename', '_autoconfig.cfg');
pref('general.config.obscure_value', 0);
pref('general.config.sandbox_enabled', false);
obscure_value = 0 means the cfg is read as plain text (autoconfig historically byte-rotated the file), and sandbox_enabled = false grants it full chrome privileges — it runs with access to Components, Services, the window manager, sockets, and processes. That is exactly the power ffwebapps needs and the reason all of the runtime’s behaviour can live in one JS file.
The cfg opens with a set of defaultPref calls that establish the runtime baseline (_autoconfig.cfg:7-30):
| Pref | Value | Why |
|---|---|---|
browser.taskbarTabs.enabled |
true |
Enables the Web Apps feature itself |
toolkit.legacyUserProfileCustomizations.stylesheets |
true |
Lets the profile’s userChrome.css load |
media.hardware-video-decoding.enabled |
true |
Reaffirm GPU video decode per app |
media.ffvpx-hw.enabled |
true |
Hardware ffvpx path |
network.cookie.cookieBehavior |
0 |
Disables Total Cookie Protection so M365/SSO silent re-auth via hidden cross-origin iframes works |
dom.ipc.processCount |
4 |
Cap content processes — a single-site app doesn’t need general-browsing counts |
dom.ipc.processCount.webIsolated |
1 |
Cap isolated cross-origin process pool |
The cookieBehavior = 0 line is a deliberate trade-off documented in the source: an app profile is single-app, not general browsing, and partitioning third-party cookies breaks the silent token renewal that Microsoft 365 and many SSO providers do through a hidden iframe (the “Sign in does nothing” symptom).
For -taskbar-tab <id> to resolve, the ID must be registered in the profile’s taskbartabs/taskbartabs.json, whose shape is validated by Firefox against its own TaskbarTabs.1.schema.json on every load. taskbartabs::sync_registry (taskbartabs.rs:74-110) writes one entry per app:
{
"version": 1,
"taskbarTabs": [
{
"id": "<webapp_id UUID>",
"scopes": [{ "hostname": "teams.microsoft.com", "prefix": "/v2" }],
"userContextId": 0,
"startUrl": "https://teams.microsoft.com/v2/",
"name": "Microsoft Teams"
}
]
}
The scope is derived from the manifest (scope_from_site, taskbartabs.rs:59-70): the hostname is the site’s domain, and the optional path prefix comes from the manifest scope path (dropped when it is just /). Entries are upserted by id, and the registry tolerates a missing or corrupt file by falling back to a fresh one. This file is rewritten on every launch, so config changes always reach Firefox.
taskbartabs::write_profile_prefs (taskbartabs.rs:178-233) regenerates the profile’s user.js at launch. It is owned by ffwebapps — the header literally says “Managed by ffwebapps — do not edit” — and carries the per-app behaviour the autoconfig reads back at runtime:
ffwebapps.externalLinks.enabled — from SiteConfig::external_links (unwrap_or(true)).ffwebapps.allowedDomains — the comma-joined in-app allow-list, either the user’s list or a scope-derived default (see Link Routing & Scope).software_rendering is set): forces gfx.webrender.software, disables layers.acceleration, and turns off every hardware video-decode path. This branch takes precedence over hardware_webrtc.hardware_webrtc is set and software rendering is not): forces decode past Firefox’s GPU blocklist and enables the hardware VP8 path used by WhatsApp/Meet.user_agent is non-empty): writes general.useragent.override, with quotes and backslashes escaped.Because these are written fresh on each launch and read once at startup, a config change “applies on next relaunch” — there is no live pref-watching. See Performance Tuning for the rendering and scheduling knobs.
A taskbar-tab window is already minimal-UI: a slim toolbar with a read-only address pill plus navigation and extension buttons. userChrome.css removes the rest while keeping one thing Firefox can’t replace. The core rule hides the whole customizable toolbar area except the window controls and the URL container:
:root[taskbartab] #nav-bar-customization-target > *:not(.titlebar-buttonbox-container):not(#urlbar-container) {
display: none !important;
}
The stylesheet then recolours the chrome dark (the manifest’s light theme_color clashes with dark app UIs), removes Firefox’s 40px titlebar spacers (the normal rule that strips them is inert when tabs are hidden), and — importantly — keeps the site-identity / permission cluster. Hiding the entire urlbar removed the anchor that camera/mic/geolocation prompts drop down from, so they never appeared and grants could never be made. The fix collapses the urlbar to just #identity-box (which contains the permission anchors and the notification-popup box) and strips the address pill, the page-action buttons, and the “remove tab from taskbar” button — leaving a single clickable lock icon, like a Chromium installed-app window.
Past the prefs, the bulk of _autoconfig.cfg is runtime behaviour. It is organized into independent try blocks so a failure in one never disables the others:
| Block | Lines | Responsibility |
|---|---|---|
| External-link backstop | 145-210 |
A http-on-modify-request observer that cancels out-of-scope top-level loads and hands them to the default browser |
| Content-side router | 217-297 |
A frame script that intercepts target="_blank" / middle-click / ctrl-click at the source and routes out-of-scope links before Firefox opens a window |
| CSS/JS injection | 299-372 |
Reads ffwebapps.css / ffwebapps.js from the profile and injects them userscript-style |
| Tray IPC + window control | 377-956 |
Serves the Unix socket, owns hide/show, close-to-tray, the unread badge, and the persisted toggles |
The link-routing blocks are covered in Link Routing & Scope; the socket server, the runtime-owned window, and the KWin hide/show mechanism are covered in IPC & the Runtime-Owned Window. The unifying idea is that the runtime decides everything about its own window — there is no external script reaching in to move or close it.
One small shared object, _ffwaShared, is exported from the link block (_autoconfig.cfg:34, 141) and reused by the socket block so the tray’s “Open page in browser” command and the link router invoke the same xdg-open-based external opener.
For Ferdium/WebCatalog-style customization, the runtime injects user files if they exist in the profile (_autoconfig.cfg:299-372):
ffwebapps.css is loaded as a USER_SHEET via windowUtils.loadSheetUsingURIString — it is CSP-immune and needs no DOM mutation.ffwebapps.js runs in a content-principal sandbox (Cu.Sandbox + evalInSandbox) at DOMContentLoaded — userscript-style, also not subject to the page’s CSP.The files are read once at startup and baked into a frame script (the contents are JSON-encoded into the script source), so editing them requires an app restart. Crucially this is per-profile, not per-app: the files live at the profile root, so every app sharing a profile shares the injection. The GTK GUI surfaces this under the profile with a banner saying as much (see GTK Management GUI).
The design’s durability comes from leaning on maintained Mozilla code and from a few defensive habits in the cfg:
:root[taskbartab] selectors; the behaviour is autoconfig JS. A Firefox update doesn’t invalidate a binary patch because there isn’t one.try blocks. Link routing, injection, and the socket server each fail closed without taking the others down.taskbartabs.json and user.js are rewritten every launch from config.json, so they cannot drift out of sync with the stored Site.--link the runtime is mostly symlinks to system Firefox; uninstalling and re-linking is cheap and tracks the distro’s Firefox updates.