Skip to main content

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:
  1. Session Creation: Each profile gets its own Electron session
  2. Extension Context Setup: ElectronChromeExtensions instance is created
  3. Extension Loading: Previously installed extensions are loaded from storage
  4. 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

  1. User navigates to Chrome Web Store
  2. Clicks “Add to Chrome” button
  3. Extension manifest and permissions are retrieved
  4. Permission dialog is shown using native system dialog
  5. Extension is downloaded and installed
  6. 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

Browser Action Popup

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 };
}

Build docs developers (and LLMs) love