ffwebapps — The Firefox Runtime & Autoconfig

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.

Table of Contents

  1. Web Apps, not chrome patching
  2. How a runtime is assembled
  3. The autoconfig loader chain
  4. The taskbartabs registry
  5. Profile prefs: user.js
  6. The chromeless titlebar
  7. What _autoconfig.cfg does
  8. Per-app CSS and JS injection
  9. Why this is robust

1. Web Apps, not chrome patching

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.

2. How a runtime is assembled

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):

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.

runtime patch: how config reaches Firefox sysdata/userchrome/runtime/ defaults/pref/autoconfig.js points Firefox at _autoconfig.cfg _autoconfig.cfg the privileged runtime JS (profile/chrome/userChrome.css) userdata/runtime/ (Firefox) firefox / firefox-bin (copied) everything else → symlinked /usr/lib/firefox + autoconfig.js + _autoconfig.cfg copied in by Runtime::patch patch userdata/profiles/<ulid>/ user.js (write_profile_prefs) chrome/userChrome.css taskbartabs/taskbartabs.json scope + start URL (sync_registry) firefox -profile <profile> -taskbar-tab <webapp_id> -new-window <start_url> -container 0 env MOZ_APP_REMOTINGNAME=ffwebapps-<ulid> = single instance the running Web App window (Firefox loads _autoconfig.cfg at startup) socket server nsIServerSocket on ffwebapps-<ULID>.sock hide/show, close-to-tray, unread, quit link router content frame script + http-on-modify-request out-of-scope → xdg-open injector ffwebapps.css → USER_SHEET ffwebapps.js → sandbox at DOMContentLoaded prefs in effect taskbarTabs.enabled cookieBehavior=0 processCount caps

3. The autoconfig loader chain

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).

4. The taskbartabs registry

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.

5. Profile prefs: user.js

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:

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.

6. The chromeless titlebar

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.

7. What _autoconfig.cfg does

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.

8. Per-app CSS and JS injection

For Ferdium/WebCatalog-style customization, the runtime injects user files if they exist in the profile (_autoconfig.cfg:299-372):

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).

9. Why this is robust

The design’s durability comes from leaning on maintained Mozilla code and from a few defensive habits in the cfg: