Security Model
Both the filesystem and shell layers are mode-based with security-first defaults.
Filesystem — useFs (fs:readFile / fs:writeFile)
fs.mode | Behavior |
|---|---|
'all' | Unrestricted — any path (path.resolve(filePath)). Use only in trusted apps. |
'allowed' (default) | Every path is confined to allowedRoots. isInsideRoot() + path.relative block traversal (..). Relative paths resolve against the first root. Throws if outside all roots. |
'none' | All filesystem access rejected. |
Shell — useShell (shell-execute)
shell.mode | Behavior |
|---|---|
'all' | Any executable. Still spawn(cmd, args, { shell:false }) → no shell injection. |
'allowed' | Only executables listed in allowedCommands. |
'none' (default) | Shell fully disabled. |
requireConsent(default true): every command shows a native Electron confirmation dialog before running. Payload is{ command, args }.
Template defaults (secure out of the box)
registerNextOP(mainWindow, {fs: { mode: 'allowed', allowedRoots: [app.getPath('userData')] },shell: { mode: 'none', allowedCommands: [], requireConsent: true },})
Navigation guards & sandbox
sandbox: trueon the main window (template) and on internally opened windows (library).attachNavigationGuards:will-navigateto a non-localhost/127.0.0.1origin is blocked (external http(s) opened in the system browser);setWindowOpenHandlerdenies all popups (intentional internal windows go through theopen-internal-windowIPC channel). Guards attach to the existingmainWindow.webContentsand to every futureweb-contents-created.- CSP is intentionally not auto-applied — a strict CSP breaks Next.js (HMR
eval, inline styles). Treat it as per-app, opt-in.
Because the Next.js backend runs on the user's machine
A built NextOP app runs a live Next.js server — API routes, Route Handlers, Server Components, Server Actions, and middleware all execute server-side, in the Electron main process, on the end user's machine. Therefore:
- The server binds to
127.0.0.1only (not the LAN). Do not change this. - Never embed central/shared DB credentials in app code or
.env— the package ships readable (asar: false), so every client would hold the key. Use a remote API with per-user tokens; an embedded local DB (SQLite) for local data is fine. - The localhost API has no auth by default and is reachable by other local processes — add your own auth to sensitive Route Handlers.
IPC channel allowlist (preload)
preload.ts does not forward arbitrary channels. The generic desktop.ipcRenderer bridge validates
every invoke/send/on against an allowlist. User-defined channels are allowed under the
app: prefix convention. on returns an unsubscribe function (prevents listener leaks).