Para crear una app de escritorio a partir de una app Capacitor, envuelve el mismo build web (webDir) en un shell de Electron: un proceso principal que abre una BrowserWindow, un script de preload que expone una pequeña API de escritorio y electron-builder para generar instaladores para macOS, Windows y Linux. Tu UI y lógica de negocio se comparten con iOS y Android, y las funciones exclusivas de escritorio viven en el proceso principal de Electron.
Esta guía cubre el estado actual de Capacitor en Electron en 2026, una configuración segura que funciona con Capacitor 8, cómo se comportan los plugins, el empaquetado y la firma, y cómo publicar actualizaciones web sin nuevos instaladores.
El estado de Capacitor en Electron en 2026
Durante años, la respuesta por defecto era @capacitor-community/electron, que conectaba Electron al Capacitor CLI como una plataforma. Ese proyecto ahora está marcado como no mantenido en su propio README. Su última versión de npm, 5.0.1, se lanzó en septiembre de 2023, requiere Capacitor 5.4 o superior y fue construido contra Electron 26. Electron ha lanzado muchas versiones principales desde entonces, y solo las tres últimas reciben correcciones de seguridad.
Aún puedes usarlo en proyectos antiguos, pero para una app Capacitor 8 la comparación se ve así:
| Enfoque | Ventajas | Contras |
|---|---|---|
| Tu propia shell de Electron (esta guía) | Electron actual, control total, sin abstracciones extra | Escribes unas 60 líneas de main y preload code |
@capacitor-community/electron |
Familiar cap comandos |
Sin mantenimiento, fijado a versiones antiguas de Capacitor y Electron |
El enfoque de shell requiere menos trabajo de lo que parece, porque Capacitor ya genera un build web estático. Electron solo necesita cargarlo.
Cómo se comportan los plugins en Electron
El renderer de Electron es Chromium, así que tu app de Capacitor se ejecuta allí como en un navegador. El core de Capacitor no detecta un bridge nativo y recurre a la implementación web de cada plugin.
| Tipo de plugin | ¿Funciona en Electron? | Qué hacer |
|---|---|---|
| Plugin con implementación web (Preferences, Share, Clipboard, muchos plugins de Capgo) | Sí, usando la versión web | Probar el comportamiento web |
| Plugin solo nativo (sin implementación web) | No, se rechaza como no implementado | Escribir un equivalente de Electron detrás de un preload API |
| Librerías puras de JS | Sí | Nada |
Capacitor.getPlatform() devuelve 'web' en esta configuración y Capacitor.isNativePlatform() devuelve false. Expondremos una bandera de escritorio explícita desde el script de preload para que tu code pueda ramificar limpiamente.
Requisitos
- Una app de Capacitor cuyo
bun run buildgenera una carpeta estática (lawebDirencapacitor.config.ts, a menudodistowww). - Bun y una versión LTS actual de Node.js, ya que las herramientas de Electron siguen ejecutándose en Node.
- Para la distribución en macOS, un certificado Apple Developer ID. Para Windows, un certificado de firma code.
Paso 1: Instalar Electron y el empaquetador
bun add -d electron electron-builder
Añade un electron/ carpeta en la raíz del proyecto:
my-app/
capacitor.config.ts
dist/ # web build, shared with iOS and Android
electron/
main.ts
preload.ts
electron-builder.yml
Paso 2: Hacer que el build web se cargue desde disco
Electron carga archivos con el file:// protocolo en producción. Dos ajustes importan:
- Rutas de assets relativas. Con Vite, configura
base: './'para que el buildindex.htmlreferencie./assets/...en lugar de/assets/.... - Routing. Deep links como
/settingsno existen como archivos. Usa hash routing para el build de escritorio o asegúrate de que tu router solo navegue en el cliente tras la cargaindex.html.
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
base: './',
});
Las rutas relativas también funcionan en iOS y Android, así que no necesitas un build separado.
Paso 3: Escribir el proceso principal
// electron/main.ts
import { app, BrowserWindow, ipcMain, shell } from 'electron';
import path from 'node:path';
const isDev = !app.isPackaged;
async function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 800,
minHeight: 600,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
win.once('ready-to-show', () => win.show());
// Open external links in the default browser, never inside the app.
// Only allow https: so a script cannot launch file: or other protocol handlers.
win.webContents.setWindowOpenHandler(({ url }) => {
try {
if (new URL(url).protocol === 'https:') void shell.openExternal(url).catch(() => {});
} catch {
// invalid URL: ignore
}
return { action: 'deny' };
});
if (isDev) {
await win.loadURL('http://localhost:5173');
win.webContents.openDevTools({ mode: 'detach' });
} else {
await win.loadFile(path.join(__dirname, '..', '..', 'dist', 'index.html'));
}
}
ipcMain.handle('desktop:get-version', () => app.getVersion());
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Mantén contextIsolation: true, nodeIntegration: false, y sandbox: true. Tu web app puede cargar scripts de terceros, y ninguno de ellos debería tener acceso a Node.js.
Paso 4: Exponer un pequeño API de escritorio desde el preload
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('desktop', {
isElectron: true,
platform: process.platform,
getVersion: () => ipcRenderer.invoke('desktop:get-version') as Promise<string>,
});
Escríbelo en tu code web y úsalo de la misma forma que usarías un plugin:
// src/desktop.ts
declare global {
interface Window {
desktop?: {
isElectron: boolean;
platform: string;
getVersion: () => Promise<string>;
};
}
}
export const isDesktop = () => Boolean(window.desktop?.isElectron);
Cada función nativa que necesites en desktop se convierte en una ipcMain.handle en el proceso principal y una función en el preload. Valida los argumentos en el proceso principal, ya que el renderer no es de confianza.
Paso 5: Build y ejecutar
Compila los archivos de Electron con Bun. El preload se ejecuta en un sandbox, así que empaquétalo como un único archivo CommonJS:
{
"main": "electron/dist/main.js",
"scripts": {
"build": "vite build",
"electron:compile": "bun build electron/main.ts --outdir electron/dist --target node --format cjs --external electron && bun build electron/preload.ts --outdir electron/dist --target node --format cjs --external electron",
"electron:dev": "bun run electron:compile && electron .",
"electron:pack": "bun run build && bun run electron:compile && electron-builder"
}
}
Durante el desarrollo, inicia tu servidor de desarrollo en una terminal (bun run dev) y bun run electron:dev en otra. Tendrás hot reload en la ventana de escritorio.
Paso 6: Empaquetar instaladores con electron-builder
# electron-builder.yml
appId: com.example.myapp
productName: My App
directories:
output: release
files:
- dist/**/*
- electron/dist/**/*
- package.json
mac:
target: [dmg, zip]
category: public.app-category.productivity
hardenedRuntime: true
win:
target: [nsis]
linux:
target: [AppImage, deb]
category: Utility
Ejecuta bun run electron:pack. electron-builder genera builds para el SO en el que se ejecuta. Usa una matriz de runners de macOS, Windows y Linux en CI para producir los tres.
| SO | Objetivos comunes | Firma |
|---|---|---|
| macOS | .dmg, .zip |
Certificado Developer ID más notarización, o Gatekeeper bloqueará la app |
| Windows | NSIS .exe, MSI |
certificado de firma de Code para evitar los avisos de SmartScreen |
| Linux | AppImage, .deb, .rpm |
Opcional |
Las aplicaciones Electron son más grandes que las builds móviles porque incluyen Chromium y Node.js. Espera instaladores de entre 80 y 150 MB, según los objetivos.
Paso 7: Publicar actualizaciones web sin nuevos instaladores
La mayoría de los cambios en una app de Capacitor son cambios web. En móvil puedes desplegarlos con actualizaciones Live Update de Capgo. En escritorio, @capgo/electron-updater hace el mismo trabajo con el mismo modelo: canales, un bundle integrado, bundles descargados y rollback automático si el nuevo bundle no llama a notifyAppReady().
bun add @capgo/electron-updater
Actualiza el proceso principal para que el updater elija el bundle a cargar:
// electron/main.ts (production branch)
import { ElectronUpdater, setupIPCHandlers, setupEventForwarding } from '@capgo/electron-updater';
const updater = new ElectronUpdater({
appId: 'com.example.myapp',
autoUpdate: true,
});
// inside createWindow(), instead of loading dist/index.html directly
const builtinPath = path.join(__dirname, '..', '..', 'dist', 'index.html');
await updater.initialize(win, builtinPath);
setupIPCHandlers(updater);
setupEventForwarding(updater, win);
await win.loadFile(updater.getCurrentBundlePath());
Añade el updater bridge al mismo archivo preload:
// electron/preload.ts (next to the desktop API)
import { exposeUpdaterAPI } from '@capgo/electron-updater/preload';
exposeUpdaterAPI();
Y confirma un arranque correcto desde la web app, solo en escritorio:
import { isDesktop } from './desktop';
if (isDesktop()) {
const { requireUpdater } = await import('@capgo/electron-updater/renderer');
await requireUpdater().notifyAppReady();
}
Sube un nuevo bundle con el mismo CLI que usas para móvil:
bun run build
bunx @capgo/cli@latest bundle upload --channel=production
Las actualizaciones web no pueden modificar Electron, el proceso principal ni el preload. Para eso, distribuye un nuevo instalador con un auto-updater de binarios. Nuestra guía sobre Auto-actualizaciones de Electron cubre esa ruta, y la página del plugin electron-updater y docs cubren todas las opciones.
Solución de problemas
Ventana en blanco en la app empaquetada. Los assets usan rutas absolutas. Configura base: './' y reconstruye. Abre DevTools en la app empaquetada para confirmar 404s en file:///assets/....
Las rutas muestran una página en blanco tras recargar. El router intentó cargar /route desde el disco. Cambia a hash routing para desktop.
require is not defined o errores de preload. El preload en sandbox no puede cargar paquetes arbitrarios en runtime. Haz un bundle en un único archivo, como en el script de compilación de arriba.
Una llamada a un plugin se rechaza con “not implemented on web”. Ese plugin no tiene implementación web. Añade un IPC handler en el proceso main y llámalo a través de window.desktop.
macOS dice que la app está dañada o es de un desarrollador no identificado. La app no está firmada ni notarizada. Configura un certificado Developer ID y la notarización en electron-builder.
Updates roll back right after install. notifyAppReady() no se llamó dentro del timeout (10 segundos por defecto). Llámalo al principio del startup de tu app.
Resumen
No necesitas un paquete de plataforma Capacitor dedicado para llegar a desktop. Un proceso main de Electron corto, un preload que exponga un API tipado, rutas de assets relativas y electron-builder te dan instaladores para los tres sistemas operativos desde el mismo build web. Añade @capgo/electron-updater y la app de desktop tiene el mismo workflow de live update que tus builds de iOS y Android. Si aún estás eligiendo un runtime de desktop, lee nuestro Comparativa de Electron y Tauri.