Cross-platform builds + GitHub Actions CI/CD + code signing + auto-update.
Build your desktop app. Push to release.
English | 한국어
Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.
Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server · Python MCP Server · Cloudflare Pages
Via create-starter (recommended):
gh repo create my-electron-app --template starter-series/electron-app-starter --clone
cd my-electron-app && npm install
npm run build
npm startOr clone directly:
git clone https://github.com/starter-series/electron-app-starter my-electron-app
cd my-electron-app && npm install
npm run build
npm startRun the headless verification loop before shipping changes:
npm run lint
npm test
npm run build
npm audit --audit-level=high
npm pack --dry-run --jsonThen build for your platform:
npm run dist├── src/
│ ├── main.js # Main process (BrowserWindow, IPC, auto-update)
│ ├── preload.js # Preload script (contextBridge + IPC whitelist)
│ ├── system-info.js # Pure handler body for the system-info channel
│ ├── shared/
│ │ └── ipc-contract.js # Canonical IPC channels + payload types
│ └── renderer/
│ ├── index.html # Renderer HTML
│ ├── renderer.js # Renderer logic (consumes window.api)
│ └── styles.css # Minimal styles
├── assets/
│ └── icon.png # App icon placeholder (replace with yours)
├── tests/
│ ├── app.test.js # Structure tests
│ ├── ipc-contract.test.js # Channel contract + preload whitelist
│ └── system-info-handler.test.js # Pure-function handler (DI mocked)
├── docs/
│ ├── CODE_SIGNING.md # macOS + Windows code signing setup
│ └── AUTO_UPDATE.md # electron-updater configuration guide
├── scripts/
│ └── bump-version.js # Semver version bumper
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Lint, test
│ │ ├── cd.yml # Cross-platform build + GitHub Release
│ │ └── setup.yml # Auto setup checklist on first use
│ └── PULL_REQUEST_TEMPLATE.md
├── eslint.config.js # ESLint v10 flat config
└── package.json
- Cross-platform desktop builds — macOS (
dmg,zip), Windows (NSIS installer), Linux (AppImage,deb) - CI pipeline —
npm audit, ESLint v10 flat config, Jest with a per-repo baseline coverage gate - CD pipeline — manual-trigger matrix build across macOS / Windows / Linux, GitHub Release with all binaries attached
- Auto-update —
electron-updateragainst GitHub Releases, with renderer-side error surfacing - Optional code signing — macOS notarization + Windows signing via GitHub Secrets
- Renderer hardening —
contextIsolation: true,nodeIntegration: false,sandbox: true, strict CSP,window.open+ cross-origin navigation blocked - IPC contract — whitelist-enforced preload bridge;
src/shared/ipc-contract.jsis the canonical contract, with preload literals drift-checked in tests because sandboxed preloads cannot require local files - Supply-chain guards —
--ignore-scriptson install,gitleakspinned by sha256, CodeQL on push/PR + weekly - Package boundary — npm
filesallowlist keeps generatedcoverage/,dist/, and install artifacts out ofnpm pack - Template UX — version bump scripts (
npm run version:patch/minor/major), auto-created setup checklist issue on first use - Tests: behavioural unit tests against the pure-function modules (
system-info,navigation-policy,shared/ipc-contract) at 95 %+ line coverage, plus structural + contract-drift guards overmain.js/preload.js. Runtime modules that touch Electron are exercised byelectron-builder --dirin CI, not by line coverage.
- Nothing publicly promised. A TypeScript migration is documented as an additive path (see What about TypeScript?) rather than scaffolded.
- Vanilla JavaScript over a plugin toolchain. LLMs can read and edit the source without first learning a framework. Forge and electron-vite are the right answer for plugin systems; this template is the right answer for "CI/CD and signing should be on by day one."
electron-builderconfigured inpackage.json. One file to point a contributor at — no separate makers/publishers surface to keep in sync.- IPC channels in a shared module. The main-process handler table registers from
src/shared/ipc-contract.js. The sandboxed preload mirrors those channel literals and the tests fail if the mirror drifts. The preload never exposes rawipcRenderer. sandbox: trueby default. Most Electron starters skip this; we treat it as load-bearing for the renderer threat model.- Per-repo baseline coverage gate. Floor is the current state, not a flat 80 % rule — keeps the gate honest when the surface area is small.
- React / Vue / Svelte with HMR inside the renderer — use electron-vite.
- The Forge plugin ecosystem (makers, publishers, plugins) — use Electron Forge.
- Pre-wired native modules with complex build requirements.
- A "batteries included" framework experience. This template stays thin so AI-assisted edits don't have to reason about hidden plugin behavior.
- None. Public template — no external persons, accounts, or internal incidents are referenced anywhere in the repo.
| Step | What it does |
|---|---|
| Security audit | npm audit for dependency vulnerabilities |
| Lint | ESLint v10 flat config over src/, tests/, scripts, and the lint config |
| Test | Jest with the template's baseline coverage gate |
| Build verification | npm run build (electron-builder --dir --publish never) |
| Workflow | What it does |
|---|---|
CodeQL (codeql.yml) |
Static analysis for security vulnerabilities (push/PR + weekly) |
Maintenance (maintenance.yml) |
Weekly CI health check — auto-creates issue on failure |
Stale (stale.yml) |
Labels inactive issues/PRs after 30 days, auto-closes after 7 more |
| Step | What it does |
|---|---|
| CI gate | Runs full CI first, build only proceeds if CI passes |
| Version guard | Fails if git tag already exists for this version |
| Matrix build | Builds on macOS, Windows, and Linux in parallel |
| Upload artifacts | Saves all platform builds as GitHub Actions artifacts |
| GitHub Release | Creates a tagged release with all platform binaries attached |
How to release:
- Bump version:
npm run version:patch(orversion:minor/version:major) - Commit and push to
main - Go to Actions tab > Build & Release > Run workflow
- When done, a GitHub Release with all platform builds is created automatically
- Existing users with auto-update receive the new version automatically
Code signing is optional. Builds work without it (apps will be unsigned). See docs/CODE_SIGNING.md for setup details.
| Secret | Description |
|---|---|
CSC_LINK |
Base64-encoded .p12 Developer ID certificate |
CSC_KEY_PASSWORD |
Certificate password |
APPLE_ID |
Apple ID email |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password for notarization |
APPLE_TEAM_ID |
Apple Developer Team ID |
| Secret | Description |
|---|---|
CSC_LINK |
Base64-encoded .pfx code signing certificate |
CSC_KEY_PASSWORD |
Certificate password |
# Run the app
npm start
# Run with logging enabled
npm run dev
# Bump version (updates package.json)
npm run version:patch # 1.0.0 → 1.0.1
npm run version:minor # 1.0.0 → 1.1.0
npm run version:major # 1.0.0 → 2.0.0
# Build for current platform
npm run build
npm run dist
# Build for specific platform
npm run dist:mac
npm run dist:win
npm run dist:linux
# Lint & test
npm run lint
npm testThe starter ships with a working IPC bridge that covers the two patterns real Electron apps need. All channel names live in src/shared/ipc-contract.js. The main process registers invoke handlers from that contract; the sandboxed preload mirrors the same literals and tests/ipc-contract.test.js fails if they drift.
1. Request / response — ipcRenderer.invoke ↔ ipcMain.handle
// src/preload.js — whitelist-enforced API on window.api
contextBridge.exposeInMainWorld('api', {
getSystemInfo() {
assertAllowed(invokeAllowed, 'system-info');
return ipcRenderer.invoke('system-info');
},
// ...
});// src/main.js — pure handler, testable without Electron
ipcMain.handle('system-info', () =>
buildSystemInfo({ os, electronApp: app, process }),
);2. Event subscription — webContents.send → ipcRenderer.on
// src/preload.js — returns an unsubscribe function
onPowerEvent(callback) {
const listener = (_e, payload) => callback(payload);
ipcRenderer.on('power-event', listener);
return () => ipcRenderer.removeListener('power-event', listener);
}// src/main.js — fan out native powerMonitor events
powerMonitor.on('suspend', () => broadcast('suspend'));
powerMonitor.on('resume', () => broadcast('resume'));Renderer usage (src/renderer/renderer.js):
window.api.getSystemInfo().then(renderInfoBlock);
const off = window.api.onPowerEvent(renderLogLine);
window.addEventListener('beforeunload', off); // always unsubscribeSecurity stance — the preload never exposes ipcRenderer itself, only the specific methods above, and rejects any channel that's not on the whitelist. The BrowserWindow runs with contextIsolation: true, nodeIntegration: false, sandbox: true, and a strict CSP (default-src 'self'). See Electron's Context Isolation docs for the threat model this protects against.
Quick reference. The "why" lives in Design intent and Non-goals; this table just lays the differences side-by-side.
| This template | Forge / electron-vite | |
|---|---|---|
| Philosophy | Thin starter with CI/CD | Full toolchain with plugins |
| Build system | electron-builder (config in package.json) |
Forge makers/publishers or Vite |
| CI/CD | Full pipeline with matrix builds + auto-update | Not included |
| Code signing | GitHub Secrets setup guide included | Manual setup |
| Auto-update | Works out of the box with GitHub Releases | Manual configuration |
| Dependencies | 1 runtime, 6 dev | 50+ |
| AI/vibe-coding | LLMs generate clean vanilla JS | LLMs must understand plugin system |
This template uses vanilla JavaScript. If you need TypeScript:
- Add
typescriptto devDependencies - Add a
tsconfig.json - Rename
.jsfiles to.ts - Update ESLint config for TypeScript
PRs welcome — please read CONTRIBUTING.md first (it spells out the design intent, lint/test/CI gates, and the "Non-goals" boundary), then use the PR template.