Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. Building Desktop Apps with Electron and Next.js
Building Desktop Apps with Electron and Next.js
HOWTOIntermediate

Building Desktop Apps with Electron and Next.js

Create cross-platform desktop applications by combining Electron for native capabilities with Next.js for the UI. Covers IPC communication, system tray,...

Dylan H.

Software Engineering

February 5, 2026
5 min read

Prerequisites

  • Next.js fundamentals
  • Node.js
  • Basic understanding of desktop app concepts

Overview

Electron lets you build native desktop apps using web technologies. Combined with Next.js, you get server-side rendering, file-based routing, and the React ecosystem in a desktop package. This guide covers the architecture from project setup to distribution.

Architecture

┌─────────────────────────────────────────────┐
│              Electron Shell                  │
│  ┌────────────────────────────────────────┐  │
│  │           Main Process                 │  │
│  │  - Window management                  │  │
│  │  - System tray                        │  │
│  │  - IPC handler                        │  │
│  │  - Native APIs (fs, shell, etc.)      │  │
│  └──────────────┬─────────────────────────┘  │
│                 │ IPC                         │
│  ┌──────────────▼─────────────────────────┐  │
│  │         Renderer Process               │  │
│  │  ┌──────────────────────────────────┐  │  │
│  │  │        Next.js App               │  │  │
│  │  │  - Pages & Components            │  │  │
│  │  │  - React State                   │  │  │
│  │  │  - UI Rendering                  │  │  │
│  │  └──────────────────────────────────┘  │  │
│  └────────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

Requirements

ComponentVersion
Node.js18+
Electron28+
Next.js14+

Process

Step 1: Project Setup

npx create-next-app@latest my-desktop-app --typescript --app
cd my-desktop-app
npm install electron electron-builder concurrently wait-on
npm install -D @types/electron

Step 2: Electron Main Process

// electron/main.ts
import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from "electron";
import path from "path";
 
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
 
const isDev = process.env.NODE_ENV === "development";
 
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      nodeIntegration: false,
    },
    titleBarStyle: "hiddenInset",
    backgroundColor: "#0a0a0a",
  });
 
  if (isDev) {
    mainWindow.loadURL("http://localhost:3000");
    mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile(path.join(__dirname, "../out/index.html"));
  }
 
  mainWindow.on("close", (event) => {
    // Minimize to tray instead of closing
    event.preventDefault();
    mainWindow?.hide();
  });
}
 
function createTray() {
  const icon = nativeImage.createFromPath(
    path.join(__dirname, "../assets/tray-icon.png")
  );
  tray = new Tray(icon.resize({ width: 16, height: 16 }));
 
  const contextMenu = Menu.buildFromTemplate([
    { label: "Show App", click: () => mainWindow?.show() },
    { type: "separator" },
    {
      label: "Quit",
      click: () => {
        mainWindow?.destroy();
        app.quit();
      },
    },
  ]);
 
  tray.setContextMenu(contextMenu);
  tray.on("double-click", () => mainWindow?.show());
}
 
app.whenReady().then(() => {
  createWindow();
  createTray();
  registerIpcHandlers();
});

Step 3: Preload Script (Secure Bridge)

// electron/preload.ts
import { contextBridge, ipcRenderer } from "electron";
 
// Expose safe APIs to the renderer
contextBridge.exposeInMainWorld("electronAPI", {
  // File operations
  readFile: (path: string) => ipcRenderer.invoke("fs:read", path),
  writeFile: (path: string, data: string) =>
    ipcRenderer.invoke("fs:write", path, data),
  selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
 
  // System info
  getPlatform: () => process.platform,
  getVersion: () => ipcRenderer.invoke("app:version"),
 
  // Events from main process
  onUpdateAvailable: (callback: (info: any) => void) =>
    ipcRenderer.on("update-available", (_, info) => callback(info)),
});

Step 4: IPC Handlers

// electron/ipc-handlers.ts
import { ipcMain, dialog, app } from "electron";
import fs from "fs/promises";
import path from "path";
 
export function registerIpcHandlers() {
  // File system operations
  ipcMain.handle("fs:read", async (_, filePath: string) => {
    // Validate path is within allowed directories
    const resolved = path.resolve(filePath);
    const content = await fs.readFile(resolved, "utf-8");
    return content;
  });
 
  ipcMain.handle("fs:write", async (_, filePath: string, data: string) => {
    const resolved = path.resolve(filePath);
    await fs.writeFile(resolved, data, "utf-8");
    return true;
  });
 
  // Native dialogs
  ipcMain.handle("dialog:selectDirectory", async () => {
    const result = await dialog.showOpenDialog({
      properties: ["openDirectory"],
    });
    return result.canceled ? null : result.filePaths[0];
  });
 
  // App info
  ipcMain.handle("app:version", () => app.getVersion());
}

Step 5: Using Electron APIs in React

// hooks/useElectron.ts
"use client";
 
import { useState, useEffect } from "react";
 
interface ElectronAPI {
  readFile: (path: string) => Promise<string>;
  writeFile: (path: string, data: string) => Promise<boolean>;
  selectDirectory: () => Promise<string | null>;
  getPlatform: () => string;
  getVersion: () => Promise<string>;
}
 
export function useElectron() {
  const [isElectron, setIsElectron] = useState(false);
 
  useEffect(() => {
    setIsElectron(typeof window !== "undefined" && "electronAPI" in window);
  }, []);
 
  const api = isElectron
    ? (window as any).electronAPI as ElectronAPI
    : null;
 
  return { isElectron, api };
}
// app/page.tsx
"use client";
 
import { useElectron } from "@/hooks/useElectron";
 
export default function Home() {
  const { isElectron, api } = useElectron();
 
  const handleSelectFolder = async () => {
    if (!api) return;
    const dir = await api.selectDirectory();
    if (dir) {
      console.log("Selected:", dir);
    }
  };
 
  return (
    <main>
      <h1>My Desktop App</h1>
      {isElectron ? (
        <button onClick={handleSelectFolder}>Select Folder</button>
      ) : (
        <p>Running in browser mode</p>
      )}
    </main>
  );
}

Step 6: Package Scripts

{
  "scripts": {
    "dev": "concurrently \"next dev\" \"wait-on http://localhost:3000 && electron .\"",
    "build": "next build && next export && electron-builder",
    "dist:win": "electron-builder --win",
    "dist:mac": "electron-builder --mac",
    "dist:linux": "electron-builder --linux"
  },
  "build": {
    "appId": "com.yourapp.desktop",
    "productName": "My Desktop App",
    "files": ["electron/**/*", "out/**/*"],
    "directories": { "output": "dist" },
    "win": { "target": "nsis" },
    "mac": { "target": "dmg" },
    "linux": { "target": "AppImage" }
  }
}

Security Best Practices

PracticeWhy
contextIsolation: truePrevents renderer from accessing Node.js
nodeIntegration: falseNo direct Node.js in browser context
Validate IPC inputsPrevent path traversal in file operations
Use preload scriptsControlled bridge between main and renderer

Key Takeaways

  • Keep the main process lean — UI logic belongs in the renderer
  • Use IPC for all communication between main and renderer processes
  • Context isolation + preload scripts = secure architecture
  • The app works as a web app in the browser and as a desktop app in Electron
  • Use electron-builder for cross-platform packaging and auto-updates
#Electron#Next.js#Desktop#Cross-Platform#TypeScript

Related Articles

Building Offline-First PWAs with Next.js and SQLite

Learn how to build a Progressive Web App with offline-first architecture using Next.js, SQLite for local storage, and Supabase for cloud sync. Includes...

5 min read

IPTV Stream Validation and M3U Playlist Management with

Build a robust IPTV stream management system using FFmpeg for validation, M3U playlist parsing, EPG integration, and automated health monitoring of live...

5 min read

Building a Content Platform with Next.js 16 and

Create a full-featured technical blog platform using Next.js 16 App Router and Contentlayer2 for MDX content management. Covers document types, search,...

5 min read
Back to all HOWTOs