🎯

electron-base

🎯Skill

from jezweb/claude-skills

VibeIndex|
What it does

Builds secure, cross-platform desktop apps with Electron 33, Vite, React, and TypeScript, featuring type-safe IPC and native module support.

πŸ“¦

Part of

jezweb/claude-skills(101 items)

electron-base

Installation

npm installInstall npm package
npm install better-sqlite3
npm runRun npm script
npm run dev
npxRun with npx
npx electron-rebuild -f -w better-sqlite3
πŸ“– Extracted from docs: jezweb/claude-skills
68Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

Overview

# Electron Base Skill

Build secure, modern desktop applications with Electron 33, Vite, React, and TypeScript.

Quick Start

1. Initialize Project

```bash

# Create Vite project

npm create vite@latest my-app -- --template react-ts

cd my-app

# Install Electron dependencies

npm install electron electron-store

npm install -D vite-plugin-electron vite-plugin-electron-renderer electron-builder

```

2. Project Structure

```

my-app/

β”œβ”€β”€ electron/

β”‚ β”œβ”€β”€ main.ts # Main process entry

β”‚ β”œβ”€β”€ preload.ts # Preload script (contextBridge)

β”‚ └── ipc-handlers/ # Modular IPC handlers

β”‚ β”œβ”€β”€ auth.ts

β”‚ └── store.ts

β”œβ”€β”€ src/ # React app (renderer)

β”œβ”€β”€ vite.config.ts # Dual-entry Vite config

β”œβ”€β”€ electron-builder.json # Build config

└── package.json

```

3. Package.json Updates

```json

{

"main": "dist-electron/main.mjs",

"scripts": {

"dev": "vite",

"build": "vite build",

"preview": "electron .",

"package": "electron-builder"

}

}

```

---

Architecture Patterns

Main vs Renderer Process Separation

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ MAIN PROCESS β”‚

β”‚ (Node.js + Electron APIs) β”‚

β”‚ - File system access β”‚

β”‚ - Native modules (better-sqlite3) β”‚

β”‚ - System dialogs β”‚

β”‚ - Protocol handlers β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”‚ IPC (invoke/handle)

β”‚ Events (send/on)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ PRELOAD SCRIPT β”‚

β”‚ (contextBridge.exposeInMainWorld) β”‚

β”‚ - Type-safe API exposed to renderer β”‚

β”‚ - No direct ipcRenderer exposure β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”‚ window.electron.*

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ RENDERER PROCESS β”‚

β”‚ (Browser context - React app) β”‚

β”‚ - No Node.js APIs β”‚

β”‚ - Uses window.electron.* for IPC β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

Type-Safe IPC Pattern

The preload script exposes a typed API to the renderer:

```typescript

// electron/preload.ts

export interface ElectronAPI {

auth: {

startOAuth: (provider: 'google' | 'github') => Promise;

getSession: () => Promise;

logout: () => Promise;

onSuccess: (callback: (session: Session) => void) => () => void;

onError: (callback: (error: string) => void) => () => void;

};

app: {

getVersion: () => Promise;

openExternal: (url: string) => Promise;

};

}

// Expose to renderer

contextBridge.exposeInMainWorld('electron', electronAPI);

// Global type declaration

declare global {

interface Window {

electron: ElectronAPI;

}

}

```

---

Security Best Practices

REQUIRED: Context Isolation

Always enable context isolation and disable node integration:

```typescript

// electron/main.ts

const mainWindow = new BrowserWindow({

webPreferences: {

preload: join(__dirname, 'preload.cjs'),

contextIsolation: true, // REQUIRED - isolates preload from renderer

nodeIntegration: false, // REQUIRED - no Node.js in renderer

sandbox: false, // May need to disable for native modules

},

});

```

NEVER: Hardcode Encryption Keys

```typescript

// WRONG - hardcoded key is a security vulnerability

const store = new Store({

encryptionKey: 'my-secret-key', // DO NOT DO THIS

});

// CORRECT - derive from machine ID

import { machineIdSync } from 'node-machine-id';

const store = new Store({

encryptionKey: machineIdSync().slice(0, 32), // Machine-unique key

});

```

Sandbox Trade-offs

Native modules like better-sqlite3 require sandbox: false. Document this trade-off:

```typescript

webPreferences: {

sandbox: false, // Required for better-sqlite3 - document security trade-off

}

```

Modules requiring sandbox: false:

  • better-sqlite3
  • node-pty
  • native-keymap

Modules working with sandbox: true:

  • electron-store (pure JS)
  • keytar (uses Electron's safeStorage)

---

OAuth with Custom Protocol Handlers

1. Register Protocol (main.ts)

```typescript

// In development, need to pass executable path

if (process.defaultApp) {

if (process.argv.length >= 2) {

app.setAsDefaultProtocolClient('myapp', process.execPath, [process.argv[1]]);

}

} else {

app.setAsDefaultProtocolClient('myapp');

}

```

2. Handle Protocol URL

```typescript

// Single instance lock (required for reliable protocol handling)

const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {

app.quit();

} else {

app.on('second-instance', (_event, commandLine) => {

const url = commandLine.find((arg) => arg.startsWith('myapp://'));

if (url) handleProtocolUrl(url);

if (mainWindow?.isMinimized()) mainWindow.restore();

mainWindow?.focus();

});

}

// macOS handles protocol differently

app.on('open-url', (_event, url) => {

handleProtocolUrl(url);

});

function handleProtocolUrl(url: string) {

const parsedUrl = new URL(url);

if (parsedUrl.pathname.includes('/auth/callback')) {

const token = parsedUrl.searchParams.get('token');

const state = parsedUrl.searchParams.get('state');

const error = parsedUrl.searchParams.get('error');

if (error) {

mainWindow?.webContents.send('auth:error', error);

} else if (token && state) {

handleAuthCallback(token, state)

.then((session) => mainWindow?.webContents.send('auth:success', session))

.catch((err) => mainWindow?.webContents.send('auth:error', err.message));

}

}

}

```

3. State Validation for CSRF Protection

```typescript

// Start OAuth - generate and store state

ipcMain.handle('auth:start-oauth', async (_event, provider) => {

const state = crypto.randomUUID();

store.set('pendingState', state);

const authUrl = ${BACKEND_URL}/api/auth/signin/${provider}?state=${state};

await shell.openExternal(authUrl);

});

// Verify state on callback

export async function handleAuthCallback(token: string, state: string): Promise {

const pendingState = store.get('pendingState');

if (state !== pendingState) {

throw new Error('State mismatch - possible CSRF attack');

}

store.set('pendingState', null);

// ... rest of auth flow

}

```

---

Native Module Compatibility

better-sqlite3

Requires rebuilding for Electron's Node ABI:

```bash

# Install

npm install better-sqlite3

# Rebuild for Electron

npm install -D electron-rebuild

npx electron-rebuild -f -w better-sqlite3

```

Vite config - externalize native modules:

```typescript

// vite.config.ts

electron({

main: {

entry: 'electron/main.ts',

vite: {

build: {

rollupOptions: {

external: ['electron', 'better-sqlite3', 'electron-store'],

},

},

},

},

});

```

electron-store

Works with sandbox enabled, but encryption key should be machine-derived:

```typescript

import Store from 'electron-store';

import { machineIdSync } from 'node-machine-id';

interface StoreSchema {

session: Session | null;

settings: Settings;

}

const store = new Store({

name: 'myapp-data',

encryptionKey: machineIdSync().slice(0, 32),

defaults: {

session: null,

settings: { theme: 'system' },

},

});

```

---

Build and Packaging

electron-builder.json

```json

{

"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",

"appId": "com.yourcompany.myapp",

"productName": "MyApp",

"directories": {

"output": "release"

},

"files": [

"dist/*/",

"dist-electron/*/"

],

"mac": {

"category": "public.app-category.productivity",

"icon": "build/icon.icns",

"hardenedRuntime": true,

"gatekeeperAssess": false,

"entitlements": "build/entitlements.mac.plist",

"entitlementsInherit": "build/entitlements.mac.plist",

"target": [

{ "target": "dmg", "arch": ["x64", "arm64"] }

],

"protocols": [

{ "name": "MyApp", "schemes": ["myapp"] }

]

},

"win": {

"icon": "build/icon.ico",

"target": [

{ "target": "nsis", "arch": ["x64"] }

]

},

"linux": {

"icon": "build/icons",

"target": ["AppImage"],

"category": "Office"

}

}

```

macOS Entitlements (build/entitlements.mac.plist)

```xml

com.apple.security.cs.allow-jit

com.apple.security.cs.allow-unsigned-executable-memory

com.apple.security.cs.disable-library-validation

```

Build Commands

```bash

# Development

npm run dev

# Production build

npm run build

# Package for current platform

npm run package

# Package for specific platform

npx electron-builder --mac

npx electron-builder --win

npx electron-builder --linux

```

---

Known Issues and Prevention

1. "Cannot read properties of undefined" in Preload

Cause: Accessing window.electron before preload completes.

Fix: Use optional chaining or check for existence:

```typescript

// In React component

useEffect(() => {

if (!window.electron?.auth) return;

const unsubscribe = window.electron.auth.onSuccess((session) => {

setSession(session);

});

return unsubscribe;

}, []);

```

2. NODE_MODULE_VERSION Mismatch

Cause: Native module compiled for different Node.js version than Electron uses.

Fix:

```bash

# Rebuild native modules for Electron

npx electron-rebuild -f -w better-sqlite3

# Or add to package.json postinstall

"scripts": {

"postinstall": "electron-rebuild"

}

```

3. OAuth State Mismatch

Cause: State not persisted or lost between OAuth start and callback.

Fix: Use persistent storage (electron-store) not memory:

```typescript

// WRONG - state lost if app restarts

let pendingState: string | null = null;

// CORRECT - persisted storage

const store = new Store({ ... });

store.set('pendingState', state);

```

4. Sandbox Conflicts with Native Modules

Cause: Sandbox prevents loading native .node files.

Fix: Disable sandbox (with documented trade-off) or use pure-JS alternatives:

```typescript

webPreferences: {

sandbox: false, // Required for better-sqlite3

// Alternative: Use sql.js (WASM) if sandbox required

}

```

5. Dual Auth System Maintenance Burden

Cause: Configuring better-auth but using manual OAuth creates confusion.

Fix: Choose one approach:

  • Use better-auth fully OR
  • Use manual OAuth only (remove better-auth)

6. Token Expiration Without Refresh

Cause: Hardcoded expiration with no refresh mechanism.

Fix: Implement token refresh or sliding sessions:

```typescript

// Check expiration with buffer

const session = store.get('session');

const expiresAt = new Date(session.expiresAt);

const bufferMs = 5 60 1000; // 5 minutes

if (Date.now() > expiresAt.getTime() - bufferMs) {

await refreshToken(session.token);

}

```

7. Empty Catch Blocks Masking Failures

Cause: Swallowing errors silently.

Fix: Log errors, distinguish error types:

```typescript

// WRONG

try {

await fetch(url);

} catch {

// Silent failure - user has no idea what happened

}

// CORRECT

try {

await fetch(url);

} catch (err) {

if (err instanceof TypeError) {

console.error('[Network] Offline or DNS failure:', err.message);

} else {

console.error('[Auth] Unexpected error:', err);

}

throw err; // Re-throw for caller to handle

}

```

8. Hardcoded Encryption Key

Cause: Using string literal for encryption.

Fix: Derive from machine identifier:

```typescript

import { machineIdSync } from 'node-machine-id';

const store = new Store({

encryptionKey: machineIdSync().slice(0, 32),

});

```

---

Vite Configuration

Full vite.config.ts

```typescript

import { defineConfig } from 'vite';

import react from '@vitejs/plugin-react';

import tailwindcss from '@tailwindcss/vite';

import electron from 'vite-plugin-electron/simple';

import { resolve, dirname } from 'path';

import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);

const __dirname = dirname(__filename);

export default defineConfig({

plugins: [

react(),

tailwindcss(),

electron({

main: {

entry: 'electron/main.ts',

vite: {

build: {

outDir: 'dist-electron',

rollupOptions: {

external: ['electron', 'better-sqlite3', 'electron-store'],

output: {

format: 'es',

entryFileNames: '[name].mjs',

},

},

},

},

},

preload: {

input: 'electron/preload.ts',

vite: {

build: {

outDir: 'dist-electron',

rollupOptions: {

output: {

format: 'cjs',

entryFileNames: '[name].cjs',

},

},

},

},

},

renderer: {},

}),

],

resolve: {

alias: {

'@': resolve(__dirname, 'src'),

},

},

build: {

outDir: 'dist',

},

optimizeDeps: {

include: ['react', 'react-dom'],

},

});

```

---

Dependencies Reference

Production Dependencies

```json

{

"dependencies": {

"electron-store": "^10.0.0",

"electron-updater": "^6.3.0"

},

"optionalDependencies": {

"better-sqlite3": "^11.0.0",

"node-machine-id": "^1.1.12"

}

}

```

Development Dependencies

```json

{

"devDependencies": {

"electron": "^33.0.0",

"electron-builder": "^25.0.0",

"electron-rebuild": "^3.2.9",

"vite-plugin-electron": "^0.28.0",

"vite-plugin-electron-renderer": "^0.14.0"

}

}

```

---

Security Checklist

Before release, verify:

  • [ ] contextIsolation: true in webPreferences
  • [ ] nodeIntegration: false in webPreferences
  • [ ] No hardcoded encryption keys
  • [ ] OAuth state validation implemented
  • [ ] No sensitive data in IPC channel names
  • [ ] External links open in system browser (shell.openExternal)
  • [ ] CSP headers configured for production
  • [ ] macOS hardened runtime enabled
  • [ ] Code signing configured (if distributing)
  • [ ] No empty catch blocks masking errors

---

Related Skills

  • better-auth - For backend authentication that pairs with Electron OAuth
  • cloudflare-worker-base - For building backend APIs
  • tailwind-v4-shadcn - For styling the renderer UI