All skills
jwynia avatar

/electron-best-practices

@075cb8f
by J Wyniajwynia/agent-skills160 stars
20

Guide AI agents through Electron app development with React including security patterns, type-safe IPC, React integration, packaging with code signing, and testing. Keywords: electron, electron-vite, electron-forge, contextBridge, IPC, security, react, packaging, code signing, notarization, playwright, desktop app.

Use this Skill: https://skilld.dev/gh/jwynia/agent-skills/electron-best-practices

This session only. Nothing lands on disk.

referencespackagingauto-updates.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Auto-Update Implementation

Overview

Shipping an Electron app without auto-update is shipping a dead product. Users will not manually download new versions. The electron-updater package (from the electron-builder ecosystem but compatible with any packaging tool) is the most widely used solution. It supports GitHub Releases, S3, generic HTTP servers, and differential updates out of the box.


Update Server Options

update.electronjs.org (Free, Open Source Only)

import { autoUpdater } from 'electron';
const server = 'https://update.electronjs.org';
const repo = 'your-org/your-app';
const url = `${server}/${repo}/${process.platform}-${process.arch}/${app.getVersion()}`;
autoUpdater.setFeedURL({ url });

Limitations: macOS and Windows only, no staged rollouts, no differential updates.

GitHub Releases with electron-updater

import { autoUpdater } from 'electron-updater';
autoUpdater.setFeedURL({
  provider: 'github',
  owner: 'your-org',
  repo: 'your-app',
  private: true,
  token: process.env.GH_TOKEN,
});

S3 or Generic HTTP Server

// S3 provider
autoUpdater.setFeedURL({
  provider: 's3',
  bucket: 'your-app-releases',
  region: 'us-east-1',
  path: '/releases',
});

// Generic server - host latest.yml and installer files at any URL
autoUpdater.setFeedURL({
  provider: 'generic',
  url: 'https://releases.yourapp.com/updates',
});

Main Process Integration

// main/updater.ts
import { autoUpdater, UpdateInfo } from 'electron-updater';
import { BrowserWindow, ipcMain } from 'electron';
import log from 'electron-log';

autoUpdater.logger = log;

export function setupAutoUpdater(mainWindow: BrowserWindow): void {
  autoUpdater.autoDownload = false;
  autoUpdater.autoInstallOnAppQuit = true;

  autoUpdater.on('update-available', (info: UpdateInfo) => {
    mainWindow.webContents.send('update:available', {
      version: info.version,
      releaseNotes: info.releaseNotes,
    });
  });

  autoUpdater.on('download-progress', (progress) => {
    mainWindow.webContents.send('update:progress', {
      percent: progress.percent,
      bytesPerSecond: progress.bytesPerSecond,
      transferred: progress.transferred,
      total: progress.total,
    });
  });

  autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
    mainWindow.webContents.send('update:ready', { version: info.version });
  });

  autoUpdater.on('error', (err: Error) => {
    log.error('Update error:', err);
    mainWindow.webContents.send('update:error', err.message);
  });

  // IPC handlers for renderer control
  ipcMain.handle('update:check', () => autoUpdater.checkForUpdates());
  ipcMain.handle('update:download', () => autoUpdater.downloadUpdate());
  ipcMain.handle('update:install', () => {
    setImmediate(() => autoUpdater.quitAndInstall());
  });

  // Check every 4 hours
  const FOUR_HOURS = 4 * 60 * 60 * 1000;
  setInterval(() => autoUpdater.checkForUpdates().catch(log.error), FOUR_HOURS);
  setTimeout(() => autoUpdater.checkForUpdates().catch(log.error), 10_000);
}

Preload Script Exposure

// preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronUpdater', {
  checkForUpdates: () => ipcRenderer.invoke('update:check'),
  downloadUpdate: () => ipcRenderer.invoke('update:download'),
  installUpdate: () => ipcRenderer.invoke('update:install'),
  onAvailable: (cb: (info: any) => void) =>
    ipcRenderer.on('update:available', (_e, info) => cb(info)),
  onProgress: (cb: (progress: any) => void) =>
    ipcRenderer.on('update:progress', (_e, progress) => cb(progress)),
  onReady: (cb: (info: any) => void) =>
    ipcRenderer.on('update:ready', (_e, info) => cb(info)),
  onError: (cb: (message: string) => void) =>
    ipcRenderer.on('update:error', (_e, message) => cb(message)),
});

Renderer UI Patterns

React Hook for Update State

// renderer/hooks/useAutoUpdate.ts
import { useEffect, useState } from 'react';

interface UpdateState {
  status: 'idle' | 'available' | 'downloading' | 'ready' | 'error';
  version?: string;
  percent?: number;
}

export function useAutoUpdate(): UpdateState & { install: () => void } {
  const [state, setState] = useState<UpdateState>({ status: 'idle' });

  useEffect(() => {
    window.electronUpdater.onAvailable((info) => {
      setState({ status: 'available', version: info.version });
      window.electronUpdater.downloadUpdate(); // Silent download
    });
    window.electronUpdater.onProgress((p) => {
      setState((prev) => ({ ...prev, status: 'downloading', percent: p.percent }));
    });
    window.electronUpdater.onReady((info) => {
      setState({ status: 'ready', version: info.version });
    });
    window.electronUpdater.onError(() => setState({ status: 'error' }));
  }, []);

  return { ...state, install: () => window.electronUpdater.installUpdate() };
}

Update Banner Component

function UpdateBanner() {
  const update = useAutoUpdate();

  if (update.status === 'downloading') {
    return (
      <div className="update-banner">
        <p>Downloading update... {update.percent?.toFixed(0)}%</p>
        <progress value={update.percent} max={100} />
      </div>
    );
  }
  if (update.status === 'ready') {
    return (
      <div className="update-banner">
        <p>Version {update.version} ready. Restart to apply.</p>
        <button onClick={update.install}>Restart Now</button>
      </div>
    );
  }
  return null;
}

Staged Rollouts

Gradually roll out updates by setting stagingPercentage in latest.yml:

# latest.yml - Staged rollout at 10%
version: 2.1.0
files:
  - url: YourApp-2.1.0.exe
    sha512: abc123...
    size: 58000000
stagingPercentage: 10
# Increase to 50% after 24 hours, then 100% after 48 hours

Differential Updates

electron-updater supports blockmap-based differential updates. Only changed blocks are downloaded. Blockmaps are generated automatically for NSIS and AppImage targets. Typical savings: a 60 MB app with minor changes downloads only 5-10 MB.


Security Considerations

  • Always use HTTPS for the update feed URL
  • Sign all releases -- electron-updater verifies signatures automatically
  • Never expose tokens to the renderer process
// BAD - Token accessible to renderer
contextBridge.exposeInMainWorld('config', {
  ghToken: process.env.GH_TOKEN, // NEVER do this
});

// GOOD - Token stays in main process
autoUpdater.setFeedURL({ provider: 'github', token: process.env.GH_TOKEN });

Testing Updates in Development

# Serve local update files
npx http-server ./test-updates -p 8080
// Override feed URL in development
if (!app.isPackaged) {
  autoUpdater.setFeedURL({ provider: 'generic', url: 'http://localhost:8080' });
  autoUpdater.forceDevUpdateConfig = true;
}

Or create dev-app-update.yml in the project root:

provider: generic
url: http://localhost:8080
updaterCacheDirName: your-app-updater

See Also

Source: SKILL.md on GitHub

2 warnings16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive guidance, templates, reference scripts, and architectural configurations for developing secure Electron applications with React. All components adhere strictly to security standards, and all external tools, scripts, and configurations are handled using safe practices without any malicious or suspicious patterns.

  • Socket16d

    1 alert: gptSecurity

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    35/35 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 075cb8f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago
Other metadata
compatibility
Requires Deno for analysis scripts. Applicable to any Electron project using TypeScript and React.
metadata
{
  "author": "agent-skills",
  "version": "1.0",
  "domain": "development",
  "type": "utility",
  "mode": "assistive"
}

README badge

README badge for jwynia/agent-skills/electron-best-practices