Electron-Architektur — Main Process, Renderer Process, Preload Script, IPC
Electron kombiniert Chromium (für die UI) mit Node.js (für native OS-Zugriffe) in einer einzigen Runtime. Das Sicherheitsmodell trennt dabei strikt, welcher Code auf welche Ressourcen zugreifen darf.
ArchitekturDie drei Kernkomponenten von Electron
# Electron Prozess-Architektur 2026
# ┌──────────────────────────────────────────────────────┐
# │ MAIN PROCESS (Node.js) │
# │ main.ts — App-Lifecycle, Window-Management │
# │ Vollzugriff auf Node.js + Electron APIs │
# │ ┌────────────────────┐ ┌────────────────────────┐ │
# │ │ BrowserWindow 1 │ │ BrowserWindow 2 │ │
# │ │ ┌──────────────┐ │ │ ┌──────────────────┐ │ │
# │ │ │ Preload.ts │ │ │ │ Preload.ts │ │ │
# │ │ │ contextBridge│ │ │ │ contextBridge │ │ │
# │ │ └──────────────┘ │ │ └──────────────────┘ │ │
# │ │ Renderer Process │ │ Renderer Process │ │
# │ │ (Chromium + React)│ │ (Chromium + React) │ │
# │ └────────────────────┘ └────────────────────────┘ │
# └──────────────────────────────────────────────────────┘
#
# IPC (Inter-Process Communication):
# Renderer → Main: ipcRenderer.invoke('channel', data)
# Main → Renderer: mainWindow.webContents.send('channel', data)
# Preload = sichere Bridge (kein direktes Node.js im Renderer!)
Security-Prinzip: Seit Electron 12 ist contextIsolation: true Standard. Der Renderer-Prozess hat keinen direkten Zugriff auf Node.js — nur auf das, was du explizit via contextBridge freigibst. Claude Code kennt dieses Modell und generiert immer sicheren Code.
Main Processmain.ts — App-Lifecycle und Window-Management
// src/main/main.ts
import { app, BrowserWindow, ipcMain, shell } from 'electron'
import { join } from 'path'
function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 800,
minHeight: 600,
titleBarStyle: 'hiddenInset', // macOS: natives Fenster-Look
trafficLightPosition: { x: 16, y: 16 },
webPreferences: {
preload: join(__dirname, '../preload/preload.js'),
contextIsolation: true, // PFLICHT für Sicherheit
nodeIntegration: false, // NIEMALS true in Produktion!
sandbox: true, // Zusätzliche Isolation
},
})
// Entwicklung: Vite Dev Server
if (process.env.NODE_ENV === 'development') {
win.loadURL('http://localhost:5173')
win.webContents.openDevTools()
} else {
win.loadFile(join(__dirname, '../renderer/index.html'))
}
// Externe Links im System-Browser öffnen
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
return win
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
// macOS: Fenster öffnen wenn Dock-Icon angeklickt
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
// macOS: App bleibt im Dock auch ohne Fenster
if (process.platform !== 'darwin') app.quit()
})
Preloadpreload.ts — die sichere Bridge zum Renderer
// src/preload/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
// Nur explizit freigegebene APIs sind im Renderer verfügbar
contextBridge.exposeInMainWorld('electronAPI', {
// Filesystem
readFile: (path: string) =>
ipcRenderer.invoke('fs:readFile', path),
writeFile: (path: string, content: string) =>
ipcRenderer.invoke('fs:writeFile', path, content),
// Dialog
openFileDialog: (options?: Electron.OpenDialogOptions) =>
ipcRenderer.invoke('dialog:openFile', options),
saveFileDialog: (options?: Electron.SaveDialogOptions) =>
ipcRenderer.invoke('dialog:saveFile', options),
// App-Infos
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getPlatform: () => process.platform,
// Events vom Main-Process empfangen
onUpdateAvailable: (callback: (info: unknown) => void) => {
ipcRenderer.on('update:available', (_event, info) => callback(info))
},
removeAllListeners: (channel: string) =>
ipcRenderer.removeAllListeners(channel),
})
// TypeScript-Typen für den Renderer
// → in src/renderer/electron.d.ts:
// interface Window { electronAPI: typeof window.electronAPI }
Sicherheitsregel: Niemals ipcRenderer direkt im Renderer-Code importieren. Alle IPC-Calls laufen ausschließlich über contextBridge. Claude Code hält diese Regel automatisch ein und warnt bei Abweichungen.
electron-vite Setup — Vite als Build-Tool, Hot Reload, TypeScript, React
electron-vite ist der moderne Build-Stack für Electron-Apps: Vite für Main, Preload und Renderer — mit Hot Module Replacement im Renderer und automatischem Neustart des Main Process bei Änderungen.
SetupNeues Electron-Projekt mit electron-vite
# Prompt für Claude Code:
# "Erstelle ein neues Electron-Projekt mit electron-vite,
# React + TypeScript, shadcn/ui und Tailwind CSS"
# 1. Scaffold mit offiziellem Template
npm create @quick-start/electron@latest my-electron-app -- \
--template react-ts
cd my-electron-app && npm install
# Projektstruktur die electron-vite erstellt:
# src/
# main/ → main.ts (Node.js + Electron APIs)
# preload/ → index.ts (contextBridge)
# renderer/ → React-App (index.html, main.tsx)
# electron.vite.config.ts
# electron-builder.yml
# 2. Dev-Modus starten (Hot Reload + DevTools)
npm run dev
# 3. Produktions-Build
npm run build # TypeScript kompilieren + Vite bundlen
npm run preview # Build lokal testen
Configelectron.vite.config.ts — Vite für alle drei Prozesse
// electron.vite.config.ts
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()], // node_modules nicht bundlen
build: {
rollupOptions: {
input: { index: resolve(__dirname, 'src/main/index.ts') }
}
}
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: { index: resolve(__dirname, 'src/preload/index.ts') }
}
}
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@shared': resolve('src/shared'), // Shared Types
}
},
plugins: [react()],
server: {
port: 5173,
strictPort: true,
},
build: {
rollupOptions: {
input: { index: resolve(__dirname, 'src/renderer/index.html') }
}
}
}
})
TypeScriptShared Types zwischen Main und Renderer
// src/shared/ipc-channels.ts — Typen für BEIDE Prozesse
export const IPC = {
FS_READ: 'fs:readFile',
FS_WRITE: 'fs:writeFile',
DIALOG_OPEN: 'dialog:openFile',
DIALOG_SAVE: 'dialog:saveFile',
APP_VERSION: 'app:getVersion',
UPDATE_CHECK: 'update:check',
UPDATE_INSTALL: 'update:install',
} as const
export type IpcChannel = (typeof IPC)[keyof typeof IPC]
export interface FileReadResult {
path: string
content: string
size: number
mtime: number
}
export interface UpdateInfo {
version: string
releaseDate: string
releaseNotes: string
downloadUrl: string
}
// src/renderer/src/electron.d.ts — Window-Typen für Renderer
interface Window {
electronAPI: {
readFile(path: string): Promise<FileReadResult>
writeFile(path: string, content: string): Promise<void>
openFileDialog(opts?: Electron.OpenDialogOptions): Promise<string[]>
saveFileDialog(opts?: Electron.SaveDialogOptions): Promise<string | null>
getVersion(): Promise<string>
getPlatform(): NodeJS.Platform
onUpdateAvailable(cb: (info: UpdateInfo) => void): void
removeAllListeners(channel: string): void
}
}
Shared Types: Typen in src/shared/ können von Main und Renderer importiert werden — ohne Duplikation. Claude Code legt diesen Ordner automatisch an und nutzt ihn konsequent für Kanalnamen und Interfaces.
IPC Communication — ipcMain/ipcRenderer, contextBridge, type-safe channels
IPC (Inter-Process Communication) ist der Herzschlag jeder Electron-App. Alle OS-nahen Operationen, die im Renderer angefragt werden, wandern über IPC in den Main Process — und das Ergebnis kommt typsicher zurück.
IPCipcMain-Handler im Main Process
// src/main/ipc-handlers.ts
import { ipcMain, dialog, app, BrowserWindow } from 'electron'
import { promises as fs } from 'fs'
import { IPC } from '../../shared/ipc-channels'
export function registerIpcHandlers() {
// Datei lesen — invoke/handle (Request-Response)
ipcMain.handle(IPC.FS_READ, async (_event, path: string) => {
const content = await fs.readFile(path, 'utf-8')
const stat = await fs.stat(path)
return { path, content, size: stat.size, mtime: stat.mtimeMs }
})
// Datei schreiben
ipcMain.handle(IPC.FS_WRITE, async (_event, path: string, content: string) => {
await fs.writeFile(path, content, 'utf-8')
})
// Öffnen-Dialog
ipcMain.handle(IPC.DIALOG_OPEN, async (_event, options) => {
const win = BrowserWindow.getFocusedWindow()!
const result = await dialog.showOpenDialog(win, {
properties: ['openFile', 'multiSelections'],
...options,
})
return result.canceled ? [] : result.filePaths
})
// Speichern-Dialog
ipcMain.handle(IPC.DIALOG_SAVE, async (_event, options) => {
const win = BrowserWindow.getFocusedWindow()!
const result = await dialog.showSaveDialog(win, options)
return result.canceled ? null : result.filePath
})
// App-Version
ipcMain.handle(IPC.APP_VERSION, () => app.getVersion())
}
// in main.ts registrieren:
// import { registerIpcHandlers } from './ipc-handlers'
// app.whenReady().then(() => { registerIpcHandlers(); createWindow() })
IPCIPC im Renderer — React Hook mit Typsicherheit
// src/renderer/src/hooks/useElectronFS.ts
import { useState, useCallback } from 'react'
import type { FileReadResult } from '@shared/ipc-channels'
export function useElectronFS() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const openAndRead = useCallback(async () => {
setLoading(true)
setError(null)
try {
const paths = await window.electronAPI.openFileDialog({
filters: [{ name: 'Text', extensions: ['txt', 'md', 'json'] }]
})
if (!paths.length) return null
const result: FileReadResult = await window.electronAPI.readFile(paths[0])
return result
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
return null
} finally {
setLoading(false)
}
}, [])
const saveFile = useCallback(async (content: string) => {
const path = await window.electronAPI.saveFileDialog({
defaultPath: 'document.txt',
filters: [{ name: 'Text', extensions: ['txt'] }]
})
if (!path) return false
await window.electronAPI.writeFile(path, content)
return true
}, [])
return { openAndRead, saveFile, loading, error }
}
Native APIs — Filesystem, System-Tray, Notifications, Shell, Dialog
Electron gibt deiner Web-App vollen Zugriff auf Betriebssystem-Features: vom Dateisystem über System-Tray-Icons bis zu nativen Desktop-Benachrichtigungen. Claude Code kennt alle Electron-APIs und generiert plattformübergreifend kompatiblen Code.
NativeSystem-Tray mit Kontextmenü
// src/main/tray.ts
import { Tray, Menu, app, nativeImage } from 'electron'
import { join } from 'path'
let tray: Tray | null = null
export function createTray(showWindow: () => void) {
// Icon: @2x für Retina-Displays (macOS)
const iconPath = join(__dirname, '../../resources/tray-icon.png')
const icon = nativeImage.createFromPath(iconPath)
tray = new Tray(icon)
tray.setToolTip('My Electron App')
const contextMenu = Menu.buildFromTemplate([
{
label: 'Öffnen',
click() { showWindow() },
},
{ type: 'separator' },
{
label: 'Version',
sublabel: app.getVersion(),
enabled: false,
},
{ type: 'separator' },
{
label: 'Beenden',
role: 'quit',
},
])
tray.setContextMenu(contextMenu)
// macOS: Klick auf Tray-Icon → Fenster anzeigen
tray.on('click', showWindow)
return tray
}
export function destroyTray() {
tray?.destroy()
tray = null
}
NativeDesktop-Benachrichtigungen + Shell-Integration
// src/main/notifications.ts
import { Notification, shell, app } from 'electron'
import { join } from 'path'
export function sendNotification(title: string, body: string, onClick?: () => void) {
if (!Notification.isSupported()) return
const notification = new Notification({
title,
body,
icon: join(__dirname, '../../resources/icon.png'),
silent: false,
})
if (onClick) notification.on('click', onClick)
notification.show()
}
// Shell-Utilities — im Renderer via IPC aufrufen
export function registerShellHandlers() {
const { ipcMain } = require('electron')
// URL im Standard-Browser öffnen
ipcMain.handle('shell:openExternal', async (_e, url: string) => {
await shell.openExternal(url)
})
// Datei im Finder/Explorer anzeigen
ipcMain.handle('shell:showItemInFolder', (_e, path: string) => {
shell.showItemInFolder(path)
})
// Datei mit zugehöriger App öffnen
ipcMain.handle('shell:openPath', async (_e, path: string) => {
const err = await shell.openPath(path)
if (err) throw new Error(err)
})
// App-Daten-Verzeichnis (plattformübergreifend)
ipcMain.handle('app:getUserDataPath', () => app.getPath('userData'))
ipcMain.handle('app:getDownloadsPath', () => app.getPath('downloads'))
}
Platform-Pfade: app.getPath('userData') gibt den plattformspezifischen App-Daten-Ordner zurück: ~/Library/Application Support (macOS), %APPDATA% (Windows), ~/.config (Linux). Claude Code nutzt diese API statt hardcodierter Pfade.
NativeNatives Datei-Watching mit chokidar
// src/main/file-watcher.ts — Dateiänderungen in Echtzeit
import chokidar from 'chokidar'
import { BrowserWindow } from 'electron'
const watchers = new Map<string, chokidar.FSWatcher>()
export function watchDirectory(dirPath: string, win: BrowserWindow) {
if (watchers.has(dirPath)) return
const watcher = chokidar.watch(dirPath, {
persistent: true,
ignoreInitial: true,
ignored: /(^|[\/\\])\../, // Dotfiles ignorieren
awaitWriteFinish: { stabilityThreshold: 200 },
})
watcher
.on('add', path => win.webContents.send('fs:change', { type: 'add', path }))
.on('change', path => win.webContents.send('fs:change', { type: 'change', path }))
.on('unlink', path => win.webContents.send('fs:change', { type: 'remove', path }))
watchers.set(dirPath, watcher)
}
export function stopWatching(dirPath: string) {
watchers.get(dirPath)?.close()
watchers.delete(dirPath)
}
// Beim App-Beenden alle Watcher schließen
export function closeAllWatchers() {
watchers.forEach(w => w.close())
watchers.clear()
}
Auto-Updater — electron-updater, GitHub Releases, delta updates
Mit electron-updater können Nutzer Updates im Hintergrund herunterladen und beim nächsten Start installieren. Claude Code konfiguriert den gesamten Update-Flow — inklusive UI-Benachrichtigung im Renderer und GitHub Releases als Update-Server.
Updaterelectron-updater Setup — GitHub Releases
# Installation
npm install electron-updater
# electron-builder.yml — Update-Konfiguration
# publish:
# provider: github
# owner: mein-github-user
# repo: meine-electron-app
# releaseType: release
// src/main/updater.ts
import { autoUpdater } from 'electron-updater'
import { BrowserWindow, ipcMain } from 'electron'
import log from 'electron-log'
export function setupAutoUpdater(win: BrowserWindow) {
autoUpdater.logger = log
autoUpdater.autoDownload = true // Im Hintergrund laden
autoUpdater.autoInstallOnAppQuit = true // Beim Beenden installieren
// Events → an Renderer weiterleiten
autoUpdater.on('checking-for-update', () =>
win.webContents.send('update:checking'))
autoUpdater.on('update-available', (info) =>
win.webContents.send('update:available', info))
autoUpdater.on('update-not-available', () =>
win.webContents.send('update:not-available'))
autoUpdater.on('download-progress', (progress) =>
win.webContents.send('update:progress', progress))
autoUpdater.on('update-downloaded', (info) => {
win.webContents.send('update:downloaded', info)
})
autoUpdater.on('error', (err) =>
win.webContents.send('update:error', err.message))
// IPC: Renderer kann manuell triggern
ipcMain.handle('update:check', () => autoUpdater.checkForUpdates())
ipcMain.handle('update:install', () => autoUpdater.quitAndInstall())
// Automatisch beim Start prüfen (nach 3 Sekunden)
setTimeout(() => autoUpdater.checkForUpdates(), 3000)
}
UpdaterUpdate-Banner im Renderer — React-Komponente
// src/renderer/src/components/UpdateBanner.tsx
import { useEffect, useState } from 'react'
type UpdateState = 'idle' | 'available' | 'downloading' | 'ready'
interface DownloadProgress {
percent: number
bytesPerSecond: number
transferred: number
total: number
}
export function UpdateBanner() {
const [state, setState] = useState<UpdateState>('idle')
const [version, setVersion] = useState<string>('')
const [progress, setProgress] = useState<number>(0)
useEffect(() => {
window.electronAPI.onUpdateAvailable((info: any) => {
setVersion(info.version)
setState('available')
})
// Weitere Events über contextBridge registrieren...
return () => window.electronAPI.removeAllListeners('update:available')
}, [])
if (state === 'idle') return null
return (
<div className="update-banner">
{state === 'available' && (
<>
<span>Update {version} verfügbar</span>
<button onClick={() => window.electronAPI.getVersion()}>
Jetzt laden
</button>
</>
)}
{state === 'downloading' && (
<span>Download: {Math.round(progress)}%</span>
)}
{state === 'ready' && (
<button onClick={() => window.electronAPI.getVersion()}>
Neustart & Update installieren
</button>
)}
</div>
)
}
Delta Updates: electron-updater unterstützt differenzielle Updates für macOS (NSIS auf Windows verfügbar). Nur geänderte Blöcke werden heruntergeladen — das spart Bandbreite und Zeit für deine Nutzer erheblich.
Packaging und Signing — electron-builder, Mac codesigning, Windows NSIS, Linux AppImage
electron-builder ist der Standard für Electron-Releases: ein Build für alle Plattformen, automatisches Code-Signing und Upload zu GitHub Releases. Claude Code generiert die komplette CI/CD-Pipeline inklusive Signing-Setup.
Buildelectron-builder.yml — vollständige Konfiguration
# electron-builder.yml
appId: com.meinefirma.meinapp
productName: Meine App
copyright: Copyright © 2026 Meine Firma GmbH
directories:
buildResources: resources # Icons, Installer-Bilder
output: dist-electron # Build-Output
files:
- dist/**/*
- "!**/*.map" # Source Maps ausschließen
- "!**/*.d.ts"
# macOS Konfiguration
mac:
target:
- target: dmg
- target: zip # Für auto-updater
category: public.app-category.productivity
hardenedRuntime: true # Pflicht für Notarization
gatekeeperAssess: false
entitlements: resources/entitlements.mac.plist
entitlementsInherit: resources/entitlements.mac.plist
identity: "Developer ID Application: Meine Firma (TEAM123456)"
dmg:
contents:
- x: 130, y: 220
- x: 410, y: 220, type: link, path: /Applications
# Windows Konfiguration
win:
target:
- target: nsis # Installer
- target: portable # Portable EXE
certificateSubjectName: "Meine Firma GmbH"
signingHashAlgorithms: [sha256]
timeStampServer: http://timestamp.digicert.com
nsis:
oneClick: false # Interaktiver Installer
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
shortcutName: Meine App
# Linux Konfiguration
linux:
target:
- AppImage
- deb
- rpm
category: Office
icon: resources/icon.png
# GitHub Releases
publish:
provider: github
owner: mein-github-user
repo: meine-electron-app
releaseType: release
CI/CDGitHub Actions — Build, Sign & Release für alle Plattformen
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
release:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# macOS: Code-Signing + Notarization
- name: Sign macOS (if macOS)
if: matrix.os == 'macos-latest'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PW }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.MAC_CERT_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
run: npm run release:mac
# Windows: Code-Signing mit PFX-Zertifikat
- name: Sign Windows (if Windows)
if: matrix.os == 'windows-latest'
env:
CSC_LINK: ${{ secrets.WIN_CERT_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run release:win
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run release:linux
macOS Notarization: Seit macOS 10.15 Catalina müssen Apps bei Apple notarisiert werden — sonst blockiert Gatekeeper die Installation. Der Prozess dauert 2–10 Minuten und läuft vollautomatisch über electron-builder wenn Apple ID und App-spezifisches Passwort als Secrets hinterlegt sind.
Buildpackage.json Scripts — Build-Workflow
// package.json — Scripts
{
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"typecheck": "tsc --noEmit",
// Packaging (kein Upload)
"pack": "npm run build && electron-builder --dir",
// Release (Upload zu GitHub Releases)
"release:mac": "npm run build && electron-builder --mac --publish always",
"release:win": "npm run build && electron-builder --win --publish always",
"release:linux":"npm run build && electron-builder --linux --publish always",
// Version bumpen + Git Tag erstellen (triggert CI)
"version:patch":"npm version patch && git push --follow-tags",
"version:minor":"npm version minor && git push --follow-tags",
"version:major":"npm version major && git push --follow-tags"
}
}
Claude Code Workflow: Prompt "Bumpe auf Version 1.2.0 und erstelle ein GitHub Release" — Claude Code führt npm version minor aus, pushed den Tag, und die CI/CD Pipeline baut automatisch signierte Installer für alle drei Plattformen.
VergleichElectron vs. Tauri vs. NeutralinoJS 2026
| Kriterium | Electron | Tauri 2 | NeutralinoJS |
| Bundle-Größe (min.) | ~80 MB | ~5 MB | ~2 MB |
| RAM-Verbrauch | ~120 MB | ~30 MB | ~20 MB |
| Node.js Backend | ✓ Voll | ✗ Rust | ~ Limitiert |
| npm-Ökosystem | ✓ Komplett | ~ Via Sidecar | ~ Eingeschränkt |
| Web-APIs Support | ✓ Chromium | ~ System WebView | ~ System WebView |
| macOS / Win / Linux | ✓ Alle | ✓ Alle | ✓ Alle |
| Lernkurve (Web-Devs) | ✓ Gering | ✗ Rust nötig | ~ Mittel |
| Produktionsreife | ✓ 10+ Jahre | ✓ Stabil | ~ Early |
Wann Electron wählen: Du nutzt viel Node.js, brauchst das volle npm-Ökosystem, willst keine neue Sprache (Rust) lernen, oder migrierst eine bestehende Web-App. Für maximale Performance und kleine Bundle-Größe ist Tauri 2 die Alternative — erfordert aber Rust-Kenntnisse.
Desktop-Apps mit Claude Code entwickeln
Starte dein Electron-Projekt in Minuten — inklusive IPC, Auto-Updater und signierter Releases für Mac, Windows und Linux.
14 Tage kostenlos testen →