Skip to content

FlyingTurkman/nextop-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

42 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

NextOP

The fullstack React framework for the desktop. Real Next.js on the inside, a native Electron shell on the outside.

npm npm license


NextOP bundles Next.js + Electron to build desktop applications for the React ecosystem. It combines the Next.js developer experience (App Router, SSR, Server Components, API routes, next/image) with Electron's native capabilities (filesystem, shell, notifications, clipboard, secure storage, native menus, multi-window).

Status: early / experimental. API may still change. Platform focus: Windows (PowerShell); macOS/Linux work but are less validated.

Quick start

npx create-nextop-app my-app
cd my-app
npm run dev      # compiles the Electron layer + launches the in-process Next dev server
npm run build    # next build + Electron compile + electron-builder package β†’ release/

The key idea: a live Next.js server, not a static export

NextOP does not statically export Next.js. Instead it runs a live Next.js HTTP server inside the Electron main process:

Electron app.whenReady()
  β†’ startNextServer(): next({ dir, dev }) + http.createServer β†’ 127.0.0.1:<port>
  β†’ BrowserWindow.loadURL("http://127.0.0.1:<port>")

Consequence: every Next.js feature works β€” App Router, SSR, Server Components, Route Handlers, Server Actions, middleware, next/image. This is the differentiator versus static-export-based desktop wrappers.

Cost: Chromium + Node + a live Next server is the heaviest runtime profile among comparable frameworks. This weight is inherent to the design, not a bug.

The same runtime model is used for dev (next({ dir: cwd, dev: true })) and production (next({ dir: app.getAppPath(), dev: false }) against the prebuilt .next output). The server binds to 127.0.0.1 only (never the LAN); the port prefers 3000 and falls back to an OS-assigned free port on EADDRINUSE.

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Electron Main Process ───────────────────────────┐
β”‚  app.whenReady()                                                             β”‚
β”‚    β”œβ”€ new BrowserWindow({ contextIsolation, sandbox, nodeIntegration:false })β”‚
β”‚    β”œβ”€ registerNextOP(mainWindow, options)   ← from "nextop-app/main"         β”‚
β”‚    β”‚     β”œβ”€ ipcMain handlers: fs, shell, clipboard, notification, menu,      β”‚
β”‚    β”‚     β”‚   window controls, secure-store, internal windows                 β”‚
β”‚    β”‚     └─ navigation guards (will-navigate / setWindowOpenHandler)         β”‚
β”‚    └─ startNextServer(dir, dev) β†’ live Next.js on 127.0.0.1:<port>           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚ contextBridge (preload.ts)                     β”‚ http
                β”‚ window.desktop / window.nextop                 β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Renderer (your Next.js app)                            β”‚
β”‚   Hooks: useFs, useWindow, useMenu, useShell, useNotification,              β”‚
β”‚          useClipboard, useSecureStore                                        β”‚
β”‚   Components: <Link> (nextop-app/link), <VirtualList> (nextop-app/...)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • preload.ts exposes window.desktop (namespaced helpers + a guarded generic ipcRenderer) and window.nextop ({ openExternal(url) }) via contextBridge.
  • contextIsolation: true, nodeIntegration: false, sandbox: true β€” the correct Electron security baseline.
  • Hooks degrade gracefully to null / no-op when window.desktop is absent (e.g. rendered on the web / during SSR).

Packages

Package Role
create-nextop-app CLI that scaffolds a project via npx create-nextop-app
nextop-app Runtime library (React hooks + main-process IPC) and the nextop CLI

Stack of a freshly scaffolded app: Next.js 16, React 19, Tailwind CSS v4, Electron 39, TypeScript 5, electron-builder 25.

Main-process setup β€” registerNextOP

Import from nextop-app/main and call it inside app.whenReady(), after the BrowserWindow is created.

// electron/main.ts
import { app, BrowserWindow } from "electron"
import path from "path"
import { startNextServer } from "./startNext"
import { registerNextOP } from "nextop-app/main"

let mainWindow: BrowserWindow | null = null

app.whenReady().then(async () => {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    show: false,
    backgroundColor: "#0a0a0a",
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
    },
  })

  mainWindow.once("ready-to-show", () => mainWindow?.show())

  registerNextOP(mainWindow, {
    fs:    { mode: "allowed", allowedRoots: [app.getPath("userData")] },
    shell: { mode: "none", allowedCommands: [], requireConsent: true },
  })

  const dev = !app.isPackaged
  const nextServer = await startNextServer(dev ? process.cwd() : app.getAppPath(), dev)
  await mainWindow.loadURL(`http://127.0.0.1:${nextServer.port}`)
})

Hooks

All hooks import from the package root β€” import { useFs, useWindow, ... } from 'nextop-app' β€” and are safe during SSR / on the web (they no-op or return null when window.desktop is unavailable).

Hook Purpose
useFs() Sandboxed readFile / writeFile, scoped by the fs config.
useWindow() minimize / maximize / close / isMaximized / isAvailable.
useNotification() Native OS notifications (showNotification({ title, body })).
useClipboard() readText / writeText for the system clipboard.
useShell() Run an OS executable with an explicit args array. Disabled by default.
useSecureStore() Encrypted key/value secret storage backed by Electron safeStorage.
useMenu() Set the native application menu ([menu, setMenu]).
'use client'
import { useFs, useWindow } from 'nextop-app'

export default function Page() {
  const { writeFile } = useFs()
  const { minimize, maximize, close } = useWindow()

  return (
    <main>
      <button onClick={() => writeFile('nextop.txt', 'Created by NextOP')}>Create file</button>
      <button onClick={minimize}>Minimize</button>
      <button onClick={maximize}>Maximize</button>
      <button onClick={close}>Close</button>
    </main>
  )
}

Components

  • <Link> (nextop-app/link) β€” a next/link wrapper with isExternal (opens in the system browser) and target="_blank" (opens a secure internal Electron window via internalOptions).
  • <VirtualList> (nextop-app/virtual-list) β€” lightweight virtualization that only renders children near the viewport (IntersectionObserver, 300px overscan).

Security model

Both the filesystem and shell layers are mode-based with security-first defaults.

Layer Modes Default
Filesystem (useFs) 'all' (any path) Β· 'allowed' (confined to allowedRoots, traversal blocked) Β· 'none' 'allowed'
Shell (useShell) 'all' Β· 'allowed' (allowlist) Β· 'none'. Always spawn(cmd, args, { shell:false }) β†’ no shell injection. requireConsent shows a native confirmation dialog. 'none', requireConsent: true
  • Navigation guards: will-navigate to a non-localhost/127.0.0.1 origin is blocked (external links open in the system browser); setWindowOpenHandler denies all popups. sandbox: true on the main window and internal windows.
  • The Next.js backend runs on the user's machine. It binds to 127.0.0.1 only. Never embed central/shared DB credentials in app code or .env (the package ships readable, asar: false) β€” use a remote API with per-user tokens. The localhost API has no auth by default; add your own to sensitive Route Handlers.
  • CSP is intentionally not auto-applied β€” a strict CSP breaks Next.js (HMR eval, inline styles). Treat it as per-app, opt-in.
  • IPC channel allowlist: preload.ts validates every invoke/send/on against an allowlist; user-defined channels are allowed under the app: prefix convention.

Documentation

The full API reference β€” every hook/component, the complete security model, the IPC channel reference, CLI internals, and FAQ β€” lives in DOCS.md and on the website:

License

MIT Β© FlyingTurkman

About

NextOP is the ultimate bridge between Next.js and Electron. It combines the rapid development experience of Next.js with the powerful native capabilities of Electron to help you build high-performance desktop applications with ease.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors