Electron Internals Skill
Process Architecture
Main Process
- Node.js environment with full OS access
- Manages BrowserWindow instances
- Handles app lifecycle (startup, quit, focus)
- Single-instance lock prevents multiple app copies
Preload Script
- Runs in renderer context BUT with Node.js access
- Bridge between main and renderer via
contextBridge.exposeInMainWorld()
- Must be minimal — every import adds to startup time
Renderer Process
- Standard web environment (Chromium)
- No direct Node.js access (security)
- Communicates with main via exposed bridges
contextBridge Pattern
The preload script (packages/suite-desktop/src/preload/index.ts) exposes four separate
bridges to the renderer — not a single desktopBridge:
// packages/suite-desktop/src/preload/index.ts
contextBridge.exposeInMainWorld("ctxbridge", ctx); // main app/context API (Desktop)
contextBridge.exposeInMainWorld("menuBridge", menuBridge); // native menu event subscription
contextBridge.exposeInMainWorld("storageBridge", storageBridge); // local file storage CRUD
contextBridge.exposeInMainWorld("desktopBridge", desktopBridge); // desktop-specific operations
| Bridge |
Type |
Purpose |
ctxbridge |
Desktop |
Core context API consumed by the renderer app shell |
menuBridge |
NativeMenuBridge |
Subscribe to forwarded native menu events (addIpcEventListener) |
storageBridge |
Storage |
Local file storage: list, all, get, put, delete |
desktopBridge |
Desktop |
Desktop-specific operations (deep links, color scheme, etc.) |
// renderer — consuming a bridge
const desktopBridge = (global as { desktopBridge: Desktop }).desktopBridge;
const storageBridge = (global as { storageBridge: Storage }).storageBridge;
await storageBridge.list("layouts");
Security Rules
- Never expose
ipcRenderer directly
- Class instances do not survive the bridge — only plain functions/objects are exposed (prototypes are lost), which is why storage methods are
.bind()-attached in preload
- Each bridge method is a typed, scoped function
- No
eval(), no remote module usage
- CSP headers prevent inline scripts
BrowserWindow Management (StudioWindow)
class StudioWindow {
#window: BrowserWindow;
constructor() {
this.#window = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: false, // needed for preload Node access
},
});
}
}
Window Lifecycle
- App starts →
StudioWindow created
- Preload runs → bridges exposed
- Renderer loads → React app mounts
- Deep links → forwarded to renderer via bridge
- Close → cleanup, save state, quit
Native Menu Integration
// Main process builds menu template
const template: MenuItemConstructorOptions[] = [
{ label: "File", submenu: [
{ label: "Open File...", click: () => sendToRenderer("open-file") },
]},
];
// Renderer receives via menuBridge
menuBridge.on("menu-event", (event: ForwardedMenuEvent) => {
switch (event) {
case "open-file": // show file picker
}
});
File System Access
Layout / Storage Loading
- Local storage entries are read/written via
storageBridge (list, all, get, put, delete)
- The renderer's
DesktopLayoutLoader (packages/suite-desktop/src/renderer/services/DesktopLayoutLoader.ts) wraps these calls
Extension Loading
.foxe files in extension directory
DesktopExtensionLoader (filesystem type) reads directly via bridge
- Supports install/uninstall by copying/deleting files
Deep Links
lichtblick://open?url=https://example.com/recording.mcap
- OS protocol registration uses the legacy
foxglove scheme:
app.setAsDefaultProtocolClient("foxglove") (packages/suite-desktop/src/main/index.ts)
- Handled deep-link URLs use the
lichtblick:// scheme — the open-url handler and
second-instance argv filter both match arg.startsWith("lichtblick://")
- Recognized links include
lichtblick://open?... and lichtblick://signin-complete
- Second-instance handler re-emits
open-url and forwards to the existing window
- Parsed in renderer to open the appropriate data source
⚠️ The protocol-client registration argument ("foxglove") differs from the URL scheme the app
actually parses (lichtblick://). Do not assume they are the same string.
Build & Packaging
desktop/electronBuilderConfig.js — electron-builder configuration
desktop/webpack.config.ts — webpack for main/preload/renderer
- Output:
.dmg (macOS), .exe/.msi (Windows), .deb/.AppImage (Linux)
- Auto-update via electron-updater (if configured)
Performance Tips
- Preload weight: Keep preload imports minimal — delays window show
- IPC serialization: Large objects are serialized — prefer transferring file paths over file contents
- Window show: Use
show: false + ready-to-show event for smooth startup
- Background throttling: Electron throttles background tabs by default — respect this for power usage
1---2name: electron-internals3description: Deep Electron implementation knowledge: main/renderer process communication, contextBridge patterns, BrowserWindow lifecycle, native menu integration, and security considerations.4---56# Electron Internals Skill78## Process Architecture910### Main Process11- Node.js environment with full OS access12- Manages BrowserWindow instances13- Handles app lifecycle (startup, quit, focus)14- Single-instance lock prevents multiple app copies1516### Preload Script17- Runs in renderer context BUT with Node.js access18- Bridge between main and renderer via `contextBridge.exposeInMainWorld()`19- Must be minimal — every import adds to startup time2021### Renderer Process22- Standard web environment (Chromium)23- No direct Node.js access (security)24- Communicates with main via exposed bridges2526## contextBridge Pattern2728The preload script (`packages/suite-desktop/src/preload/index.ts`) exposes **four** separate29bridges to the renderer — not a single `desktopBridge`:3031```typescript32// packages/suite-desktop/src/preload/index.ts33contextBridge.exposeInMainWorld("ctxbridge", ctx); // main app/context API (Desktop)34contextBridge.exposeInMainWorld("menuBridge", menuBridge); // native menu event subscription35contextBridge.exposeInMainWorld("storageBridge", storageBridge); // local file storage CRUD36contextBridge.exposeInMainWorld("desktopBridge", desktopBridge); // desktop-specific operations37```3839| Bridge | Type | Purpose |40|--------|------|---------|41| `ctxbridge` | `Desktop` | Core context API consumed by the renderer app shell |42| `menuBridge` | `NativeMenuBridge` | Subscribe to forwarded native menu events (`addIpcEventListener`) |43| `storageBridge` | `Storage` | Local file storage: `list`, `all`, `get`, `put`, `delete` |44| `desktopBridge` | `Desktop` | Desktop-specific operations (deep links, color scheme, etc.) |4546```typescript47// renderer — consuming a bridge48const desktopBridge = (global as { desktopBridge: Desktop }).desktopBridge;49const storageBridge = (global as { storageBridge: Storage }).storageBridge;50await storageBridge.list("layouts");51```5253### Security Rules54- Never expose `ipcRenderer` directly55- Class instances do not survive the bridge — only plain functions/objects are exposed (prototypes are lost), which is why storage methods are `.bind()`-attached in preload56- Each bridge method is a typed, scoped function57- No `eval()`, no `remote` module usage58- CSP headers prevent inline scripts5960## BrowserWindow Management (StudioWindow)6162```typescript63class StudioWindow {64 #window: BrowserWindow;6566 constructor() {67 this.#window = new BrowserWindow({68 webPreferences: {69 preload: path.join(__dirname, "preload.js"),70 contextIsolation: true,71 nodeIntegration: false,72 sandbox: false, // needed for preload Node access73 },74 });75 }76}77```7879### Window Lifecycle801. App starts → `StudioWindow` created812. Preload runs → bridges exposed823. Renderer loads → React app mounts834. Deep links → forwarded to renderer via bridge845. Close → cleanup, save state, quit8586## Native Menu Integration8788```typescript89// Main process builds menu template90const template: MenuItemConstructorOptions[] = [91 { label: "File", submenu: [92 { label: "Open File...", click: () => sendToRenderer("open-file") },93 ]},94];9596// Renderer receives via menuBridge97menuBridge.on("menu-event", (event: ForwardedMenuEvent) => {98 switch (event) {99 case "open-file": // show file picker100 }101});102```103104## File System Access105106### Layout / Storage Loading107- Local storage entries are read/written via `storageBridge` (`list`, `all`, `get`, `put`, `delete`)108- The renderer's `DesktopLayoutLoader` (`packages/suite-desktop/src/renderer/services/DesktopLayoutLoader.ts`) wraps these calls109110### Extension Loading111- `.foxe` files in extension directory112- `DesktopExtensionLoader` (filesystem type) reads directly via bridge113- Supports install/uninstall by copying/deleting files114115## Deep Links116117```118lichtblick://open?url=https://example.com/recording.mcap119```120121- **OS protocol registration** uses the legacy `foxglove` scheme:122 `app.setAsDefaultProtocolClient("foxglove")` (`packages/suite-desktop/src/main/index.ts`)123- **Handled deep-link URLs** use the `lichtblick://` scheme — the `open-url` handler and124 second-instance argv filter both match `arg.startsWith("lichtblick://")`125- Recognized links include `lichtblick://open?...` and `lichtblick://signin-complete`126- Second-instance handler re-emits `open-url` and forwards to the existing window127- Parsed in renderer to open the appropriate data source128129> ⚠️ The protocol-client registration argument (`"foxglove"`) differs from the URL scheme the app130> actually parses (`lichtblick://`). Do not assume they are the same string.131132## Build & Packaging133134- `desktop/electronBuilderConfig.js` — electron-builder configuration135- `desktop/webpack.config.ts` — webpack for main/preload/renderer136- Output: `.dmg` (macOS), `.exe`/`.msi` (Windows), `.deb`/`.AppImage` (Linux)137- Auto-update via electron-updater (if configured)138139## Performance Tips1401411. **Preload weight**: Keep preload imports minimal — delays window show1422. **IPC serialization**: Large objects are serialized — prefer transferring file paths over file contents1433. **Window show**: Use `show: false` + `ready-to-show` event for smooth startup1444. **Background throttling**: Electron throttles background tabs by default — respect this for power usage