Introduction
Flow Browser provides comprehensive Chrome extension support through the electron-chrome-extensions library. This integration enables users to install and run Chrome extensions directly from the Chrome Web Store, with support for both Manifest V2 and V3 extensions.
Architecture
Core Components
Flow Browser’s extension system is built on several key components:
- ElectronChromeExtensions: Main integration layer that provides Chrome Extension APIs
- ExtensionManager: Manages extension lifecycle, loading, and persistence
- Chrome Web Store Integration: Enables direct installation from the Chrome Web Store
- CRX Protocol Handler: Custom protocol for serving extension resources
Extension Storage
Extensions are stored in the profile directory structure:
{profilePath}/Extensions/
├── unpacked/ # Manually loaded unpacked extensions
│ └── {extensionId}/
└── crx/ # Chrome Web Store extensions
└── {extensionId}/
└── {version}/
Setting Up Extensions
Initialization
Extensions are initialized when a profile is loaded. The process involves:
- Session Creation: Each profile gets its own Electron session
- Extension Context Setup:
ElectronChromeExtensions instance is created
- Extension Loading: Previously installed extensions are loaded from storage
- Chrome Web Store Integration: Web Store API is installed for the session
import { ElectronChromeExtensions } from "electron-chrome-extensions";
import { installChromeWebStore } from "electron-chrome-web-store";
// Create extension context
const extensions = new ElectronChromeExtensions({
license: "GPL-3.0",
session: profileSession,
registerCrxProtocolInDefaultSession: false,
// Tab lifecycle callbacks
createTab: async (tabDetails) => {
// Create new tab in Flow Browser
},
selectTab: (tabWebContents) => {
// Switch to existing tab
},
removeTab: (tabWebContents) => {
// Close tab
},
// Window lifecycle callbacks
createWindow: async (details) => {
// Create new browser window
},
removeWindow: (electronWindow) => {
// Close window
}
});
// Install Chrome Web Store support
await installChromeWebStore({
session: profileSession,
extensionsPath: crxExtensionsPath,
minimumManifestVersion: 2, // Support MV2 and MV3
loadExtensions: false,
beforeInstall: async (details) => {
// Show permission dialog
return { action: "allow" };
},
afterInstall: async (details) => {
// Update extension registry
}
});
See: loaded-profiles-controller/index.ts:123
Profile-Session Mapping
Flow Browser uses a custom partition session grabber to map profiles to Electron sessions:
import { setPartitionSessionGrabber } from "electron-chrome-extensions";
const partitionSessionGrabber = (partition: string) => {
const PROFILE_PREFIX = "profile:";
if (partition.startsWith(PROFILE_PREFIX)) {
const profileId = partition.slice(PROFILE_PREFIX.length);
return sessionsController.getIfExists(profileId);
}
return session.fromPartition(partition);
};
setPartitionSessionGrabber(partitionSessionGrabber);
See: modules/extensions/main.ts:6
CRX Protocol
Flow Browser registers a custom crx:// protocol handler to serve extension resources across different profiles:
app.whenReady().then(() => {
protocol.handle("crx", async (request) => {
const url = URL.parse(request.url);
const partition = url?.searchParams.get("partition");
if (!partition) {
return new Response("No partition", { status: 400 });
}
const session = partitionSessionGrabber(partition);
const extensions = ElectronChromeExtensions.fromSession(session);
return extensions.handleCrxRequest(request);
});
});
See: modules/extensions/main.ts:25
Extension Manager
The ExtensionManager class handles extension lifecycle and persistence:
Loading Extensions
const extensionsManager = new ExtensionManager(
profileId,
profileSession,
extensionsPath
);
// Load all enabled extensions
await extensionsManager.loadExtensions();
Managing Extension State
// Enable/disable an extension
await extensionsManager.setExtensionDisabled(extensionId, false);
// Pin/unpin an extension to toolbar
await extensionsManager.setPinned(extensionId, true);
// Uninstall an extension
await extensionsManager.uninstallExtension(extensionId);
See: modules/extensions/management.ts:153
Chrome Web Store Integration
Flow Browser provides seamless Chrome Web Store integration:
Installation Flow
- User navigates to Chrome Web Store
- Clicks “Add to Chrome” button
- Extension manifest and permissions are retrieved
- Permission dialog is shown using native system dialog
- Extension is downloaded and installed
- Extension is registered in the profile’s extension store
Installation Hooks
await installChromeWebStore({
session: profileSession,
extensionsPath: crxExtensionsPath,
minimumManifestVersion: 2,
beforeInstall: async (details) => {
const title = `Add "${details.localizedName}"?`;
const returnValue = await dialog.showMessageBox({
title,
message: `Install extension with permissions: ${permissions}`,
icon: details.icon,
buttons: ["Cancel", "Add Extension"]
});
return { action: returnValue.response === 0 ? "deny" : "allow" };
},
afterInstall: async (details) => {
await extensionsManager.addInstalledExtension("crx", details.id);
},
afterUninstall: async (details) => {
await extensionsManager.removeInstalledExtension(details.id);
}
});
See: loaded-profiles-controller/index.ts:230
Browser Action Integration
Flow Browser injects browser action UI elements into the WebUI:
import { injectBrowserAction } from "electron-chrome-extensions/browser-action";
// Inject <browser-action-list> element into WebUI
injectBrowserAction();
This creates a <browser-action-list> custom element that displays extension icons and handles popup windows.
See: preload/index.ts:99
Extension Types
Flow Browser supports two types of extensions:
CRX Extensions
Extensions installed from the Chrome Web Store:
- Automatically updated
- Stored in versioned directories
- Managed by
electron-chrome-web-store
Unpacked Extensions
Manually loaded development extensions:
- No automatic updates
- Stored directly in extension directory
- Useful for development and testing
Currently, unpacked extension uninstallation is not fully implemented. See management.ts:416
Extension Data Storage
Extension metadata is stored in the profile’s datastore:
type ExtensionData = {
type: ExtensionType; // "crx" or "unpacked"
disabled: boolean; // Whether extension is disabled
pinned: boolean; // Whether icon is pinned to toolbar
};
The extension store is located at:
{appData}/datastore/profiles/{profileId}/extensions
See: modules/extensions/management.ts:11
Manifest Version Support
Flow Browser supports both Manifest V2 and V3 extensions:
const minimumManifestVersion =
getSettingValueById("enableMv2Extensions") ? 2 : undefined;
await installChromeWebStore({
minimumManifestVersion,
// ... other options
});
By default, both versions are supported. The enableMv2Extensions setting can restrict to MV3 only.
See: loaded-profiles-controller/index.ts:229
Service Worker Support
Manifest V3 extensions with service workers are automatically started:
if (extension.manifest.manifest_version === 3 &&
extension.manifest.background?.service_worker) {
const scope = `chrome-extension://${extension.id}`;
await session.serviceWorkers.startWorkerForScope(scope);
}
See: modules/extensions/management.ts:282
Extension Events
Flow Browser emits events when extension popups are created:
extensions.on("browser-action-popup-created", (popup) => {
if (popup.browserWindow) {
windowsController.extensionPopup.new(popup.browserWindow);
}
});
See: loaded-profiles-controller/index.ts:212
URL Overrides
Extensions can override the new tab page:
extensions.on("url-overrides-updated", (urlOverrides) => {
if (urlOverrides.newtab) {
profile.newTabUrl = urlOverrides.newtab;
}
});
See: loaded-profiles-controller/index.ts:218
Best Practices
Security ConsiderationsAlways validate extension permissions before installation. Flow Browser displays permission warnings to users, but developers should be aware of what extensions can access.
Performance Tips
- Extensions are loaded asynchronously to avoid blocking profile initialization
- Disabled extensions are not loaded into memory
- Extension service workers are started on-demand
Example: Complete Extension Setup
import { ElectronChromeExtensions } from "electron-chrome-extensions";
import { installChromeWebStore } from "electron-chrome-web-store";
import { ExtensionManager } from "@/modules/extensions/management";
export async function setupExtensions(
profileId: string,
profileSession: Session,
extensionsPath: string
) {
// Create extension manager
const extensionsManager = new ExtensionManager(
profileId,
profileSession,
extensionsPath
);
// Setup ElectronChromeExtensions
const extensions = new ElectronChromeExtensions({
license: "GPL-3.0",
session: profileSession,
assignTabDetails: (tabDetails, tabWebContents) => {
const tab = tabsController.getTabByWebContents(tabWebContents);
if (!tab) return;
tabDetails.title = tab.title;
tabDetails.url = tab.url;
tabDetails.favIconUrl = tab.faviconURL ?? undefined;
},
createTab: async (details) => {
const tab = await tabsController.createTab(
undefined,
profileId,
undefined,
undefined,
{ url: details.url }
);
return [tab.webContents!, tab.getWindow().browserWindow];
},
selectTab: (tabWebContents) => {
const tab = tabsController.getTabByWebContents(tabWebContents);
if (tab) tabsController.setActiveTab(tab);
},
removeTab: (tabWebContents) => {
const tab = tabsController.getTabByWebContents(tabWebContents);
if (tab) tab.destroy();
}
});
// Load existing extensions
await extensionsManager.loadExtensions();
// Install Chrome Web Store
await installChromeWebStore({
session: profileSession,
extensionsPath: path.join(extensionsPath, "crx"),
beforeInstall: async (details) => {
// Show permission dialog to user
return { action: "allow" };
},
afterInstall: async (details) => {
await extensionsManager.addInstalledExtension("crx", details.id);
}
});
return { extensions, extensionsManager };
}