Introduction
Flow Browser uses Electron’s IPC (Inter-Process Communication) system to enable secure communication between the main process and renderer processes. The IPC layer is fully typed using TypeScript interfaces, providing type safety and autocomplete support.
Architecture
Flow’s IPC system follows a structured, permission-based architecture:
Process Separation
Main Process (src/main/ipc/): Handles IPC requests, manages browser state, and controls system resources
Renderer Process : UI code running in browser windows that invokes IPC methods via window.flow
Preload Script (src/preload/index.ts): Bridges the two processes using Electron’s contextBridge
Key Components
IPC Handlers Main process modules that register ipcMain handlers
Typed Interfaces TypeScript interfaces defining the IPC API contracts
Preload Bridge Context bridge exposing window.flow API to renderers
Listener Manager Opt-in subscription system for push-style updates
IPC Handlers
IPC handlers are organized by functional area in src/main/ipc/. Each module registers listeners using ipcMain.on() (one-way) or ipcMain.handle() (request-response):
// src/main/ipc/browser/navigation.ts
import { ipcMain } from "electron" ;
import { tabsController } from "@/controllers/tabs-controller" ;
ipcMain . on ( "navigation:go-to" , ( event , url : string , tabId ?: number ) => {
const tab = tabId
? tabsController . getTabById ( tabId )
: tabsController . getFocusedTab ( window . id , currentSpace );
if ( tab ) {
tab . loadURL ( url );
}
});
ipcMain . handle ( "navigation:get-tab-status" , async ( _event , tabId : number ) => {
const tab = tabsController . getTabById ( tabId );
if ( ! tab ) return null ;
return {
navigationHistory: tab . webContents . navigationHistory . getAllEntries (),
activeIndex: tab . webContents . navigationHistory . getActiveIndex (),
canGoBack: tab . webContents . navigationHistory . canGoBack (),
canGoForward: tab . webContents . navigationHistory . canGoForward ()
};
});
Handler Registration
All IPC modules are imported in src/main/ipc/index.ts. Simply importing a module is enough to register its handlers:
// src/main/ipc/index.ts
import "@/ipc/app/app" ;
import "@/ipc/browser/browser" ;
import "@/ipc/browser/tabs" ;
import "@/ipc/browser/navigation" ;
import "@/ipc/session/profiles" ;
// ... more imports
Typed Interfaces
Every IPC namespace has a corresponding TypeScript interface in src/shared/flow/interfaces/. These interfaces define the API contract:
// src/shared/flow/interfaces/browser/navigation.ts
export interface FlowNavigationAPI {
/**
* Navigates to a specific URL
* @param url The URL to navigate to
* @param tabId The id of the tab to navigate (optional, uses focused tab if not provided)
*/
goTo : ( url : string , tabId ?: number ) => void ;
/**
* Gets the navigation status of a tab
* @param tabId The id of the tab to get the navigation status of
*/
getTabNavigationStatus : ( tabId : number ) => Promise < TabNavigationStatus | null >;
/**
* Stops loading a tab
* @param tabId The id of the tab to stop loading
*/
stopLoadingTab : ( tabId : number ) => void ;
/**
* Reloads a tab
* @param tabId The id of the tab to reload
*/
reloadTab : ( tabId : number ) => void ;
/**
* Navigates to a specific navigation entry
* @param tabId The id of the tab to navigate
* @param index The index of the navigation entry to navigate to
*/
goToNavigationEntry : ( tabId : number , index : number ) => void ;
}
Preload Bridge
The preload script (src/preload/index.ts) implements the typed interfaces and exposes them via contextBridge:
// src/preload/index.ts
import { contextBridge , ipcRenderer } from "electron" ;
import { FlowNavigationAPI } from "~/flow/interfaces/browser/navigation" ;
const navigationAPI : FlowNavigationAPI = {
goTo : ( url : string , tabId ?: number ) => {
return ipcRenderer . send ( "navigation:go-to" , url , tabId );
},
getTabNavigationStatus : ( tabId : number ) => {
return ipcRenderer . invoke ( "navigation:get-tab-status" , tabId );
},
stopLoadingTab : ( tabId : number ) => {
return ipcRenderer . send ( "navigation:stop-loading-tab" , tabId );
},
reloadTab : ( tabId : number ) => {
return ipcRenderer . send ( "navigation:reload-tab" , tabId );
},
goToNavigationEntry : ( tabId : number , index : number ) => {
return ipcRenderer . send ( "navigation:go-to-entry" , tabId , index );
}
};
contextBridge . exposeInMainWorld ( "flow" , {
navigation: navigationAPI ,
// ... other APIs
});
Listener Manager
For push-style updates (main → renderer broadcasts), Flow uses an opt-in subscription system managed by listeners-manager.ts:
Subscribing to Updates
Renderers must explicitly subscribe to channels they want to receive:
// Renderer process
function listenOnIPCChannel ( channel : string , callback : ( ... args : any []) => void ) {
const listenerId = generateUUID ();
// Register this listener with the main process
ipcRenderer . send ( "listeners:add" , channel , listenerId );
// Listen for messages
ipcRenderer . on ( channel , ( _event , ... args ) => {
callback ( ... args );
});
// Return cleanup function
return () => {
ipcRenderer . send ( "listeners:remove" , channel , listenerId );
ipcRenderer . removeListener ( channel , callback );
};
}
// Example usage
const unsubscribe = listenOnIPCChannel ( "tabs:on-data-changed" , ( data ) => {
console . log ( "Tabs updated:" , data );
});
// Later: clean up
unsubscribe ();
Broadcasting from Main Process
Main process code uses helper functions from listeners-manager.ts:
import { sendMessageToListeners } from "@/ipc/listeners-manager" ;
// Broadcast to all subscribed renderers
sendMessageToListeners ( "tabs:on-data-changed" , updatedTabsData );
The listener manager automatically cleans up subscriptions when WebContents is destroyed, preventing memory leaks.
Permission System
Flow’s IPC APIs are protected by a permission system based on the renderer’s protocol and hostname:
// src/preload/index.ts
type Permission = "all" | "app" | "browser" | "session" | "settings" ;
function hasPermission ( permission : Permission ) {
const isFlowProtocol = location . protocol === "flow:" ;
const isFlowInternalProtocol = location . protocol === "flow-internal:" ;
const isMainUI = location . hostname === "main-ui" && isFlowInternalProtocol ;
const isBrowserUI = isMainUI || /* ... */ ;
switch ( permission ) {
case "all" :
return true ;
case "app" :
return isFlowInternalProtocol || /* extensions */ ;
case "browser" :
return isBrowserUI || /* omnibox */ ;
case "session" :
return isFlowInternalProtocol || isBrowserUI ;
case "settings" :
return isFlowInternalProtocol ;
default :
return false ;
}
}
Permission Levels
Available to all renderers, including web pages
Available to Flow internal protocols and extension pages
Available to browser UI and omnibox
Available to Flow internal protocols, browser UI, and omnibox
Available only to Flow internal protocols
IPC Namespaces
The Flow API is organized into logical namespaces:
App APIs
window.flow.app - Application info, clipboard, default browser
window.flow.windows - Window management and controls
window.flow.extensions - Extension management
window.flow.updates - Auto-update functionality
window.flow.actions - Global actions and shortcuts
window.flow.shortcuts - Keyboard shortcut configuration
Browser APIs
window.flow.browser - Profile loading, window creation
window.flow.tabs - Tab management (create, close, move, etc.)
window.flow.navigation - Page navigation (go to URL, back, forward, reload)
window.flow.page - Page layout and bounds
window.flow.interface - UI component positioning and window controls
window.flow.omnibox - Omnibox (address bar) control
window.flow.findInPage - Find in page functionality
Session APIs
window.flow.profiles - Profile CRUD operations
window.flow.spaces - Space (tab group) management
Settings APIs
window.flow.settings - Settings storage
window.flow.icons - App icon customization
window.flow.openExternal - External link handling preferences
window.flow.onboarding - Onboarding flow control
Usage Examples
Navigating to a URL
// Navigate the focused tab
window . flow . navigation . goTo ( "https://example.com" );
// Navigate a specific tab
window . flow . navigation . goTo ( "https://example.com" , tabId );
Creating a New Tab
// Create tab in background
await window . flow . tabs . newTab ( "https://example.com" , false , spaceId );
// Create tab in foreground
await window . flow . tabs . newTab ( "https://example.com" , true , spaceId );
Listening for Tab Updates
import { useEffect , useState } from "react" ;
function TabsList () {
const [ tabs , setTabs ] = useState ([]);
useEffect (() => {
// Initial fetch
window . flow . tabs . getData (). then ( setTabs );
// Subscribe to updates
const unsubscribe = window . flow . tabs . onDataUpdated (( data ) => {
setTabs ( data );
});
return unsubscribe ;
}, []);
return (
< ul >
{ tabs . map ( tab => (
< li key = {tab. id } > {tab. title } </ li >
))}
</ ul >
);
}
Getting Window State
const state = await window . flow . windows . getCurrentWindowState ();
console . log ( "Maximized:" , state . isMaximized );
console . log ( "Fullscreen:" , state . isFullscreen );
// Listen for changes
const unsubscribe = window . flow . windows . onCurrentWindowStateChanged (( state ) => {
console . log ( "Window state changed:" , state );
});
Adding a New IPC Namespace
To add a new IPC namespace to Flow:
Create the TypeScript interface
Define your API contract in src/shared/flow/interfaces/[category]/[name].ts: // src/shared/flow/interfaces/browser/example.ts
export interface FlowExampleAPI {
/**
* Does something cool
* @param param1 Description of param1
*/
doSomething : ( param1 : string ) => Promise < boolean >;
}
Implement IPC handlers
Create handlers in src/main/ipc/[category]/[name].ts: // src/main/ipc/browser/example.ts
import { ipcMain } from "electron" ;
ipcMain . handle ( "example:do-something" , async ( _event , param1 : string ) => {
// Implementation
return true ;
});
Register handlers
Import the module in src/main/ipc/index.ts: import "@/ipc/browser/example" ;
Implement preload bridge
Add the API implementation in src/preload/index.ts: const exampleAPI : FlowExampleAPI = {
doSomething : async ( param1 : string ) => {
return ipcRenderer . invoke ( "example:do-something" , param1 );
}
};
// Add to flowAPI object
const flowAPI = {
// ...
example: wrapAPI ( exampleAPI , "browser" )
};
Use in renderer
Call the API from your React components: const result = await window . flow . example . doSomething ( "test" );
Best Practices
Use ipcMain.handle for request-response patterns
When you need a return value from the main process, use ipcMain.handle() and ipcRenderer.invoke(): // Main process
ipcMain . handle ( "get-data" , async () => {
return { data: "value" };
});
// Renderer
const data = await ipcRenderer . invoke ( "get-data" );
Use ipcMain.on for one-way messages
When you don’t need a response, use ipcMain.on() and ipcRenderer.send(): // Main process
ipcMain . on ( "log-message" , ( _event , message : string ) => {
console . log ( message );
});
// Renderer
ipcRenderer . send ( "log-message" , "Hello!" );
Always clean up listeners
Return cleanup functions from listener registration: useEffect (() => {
const unsubscribe = window . flow . tabs . onDataUpdated ( handleUpdate );
return unsubscribe ; // Clean up on unmount
}, []);
Use the listener manager for broadcasts
Instead of calling webContents.send() directly, use the listener manager to only send to subscribed renderers: import { sendMessageToListeners } from "@/ipc/listeners-manager" ;
sendMessageToListeners ( "my-channel" , data );
Validate permissions in preload
Use the wrapAPI helper to enforce permission requirements: const flowAPI = {
myAPI: wrapAPI ( myAPIImpl , "browser" ) // Requires browser permission
};
Security Considerations
Never expose ipcRenderer directly to untrusted content. Always use contextBridge in the preload script.
Validate all inputs : IPC handlers should validate and sanitize all parameters
Check permissions : Use the permission system to restrict sensitive APIs
Avoid exposing sensitive data : Don’t return credentials or secrets via IPC
Use typed interfaces : TypeScript interfaces help prevent type-related bugs
Limit broadcast scope : Use the listener manager to avoid sending updates to unsubscribed renderers
Browser IPC Profile loading and window creation APIs
App IPC Application info, clipboard, and window controls
Window IPC Window management and state APIs
Electron IPC Docs Official Electron IPC documentation