# The Desktop App: Electron Wrapper

> The Electron app bundles the FastAPI backend (as a sidecar process) and the Next.js frontend into a single installable for Mac, Windows, and Linux — no Docker needed. This page explains how the build scripts wire everything together.

- Repository: presenton/presenton
- GitHub: https://github.com/presenton/presenton
- Human wiki: https://grok-wiki.com/public/wiki/presenton-presenton-f6685dc028cc
- Complete Markdown: https://grok-wiki.com/public/wiki/presenton-presenton-f6685dc028cc/llms-full.txt

## Source Files

- `electron/app/main.ts`
- `electron/build.js`
- `electron/copy_fastapi_assets.js`
- `electron/build_nextjs_resources.js`
- `electron/package.json`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [electron/app/main.ts](electron/app/main.ts)
- [electron/build.js](electron/build.js)
- [electron/copy_fastapi_assets.js](electron/copy_fastapi_assets.js)
- [electron/build_nextjs_resources.js](electron/build_nextjs_resources.js)
- [electron/package.json](electron/package.json)
- [electron/app/utils/servers.ts](electron/app/utils/servers.ts)
- [electron/app/utils/constants.ts](electron/app/utils/constants.ts)
</details>

# The Desktop App: Electron Wrapper

Presenton ships as a conventional installable desktop application — a `.dmg` for macOS, an `AppImage`/`.deb` for Linux, and an NSIS installer or `.appx` for Windows — with no Docker or separate server setup required. The secret is that the Electron shell bundles both the FastAPI Python backend and the Next.js frontend as ordinary files inside the package, then launches them as child processes when the app opens. From the user's perspective it looks like any other native app; under the hood, three processes are running on `localhost`.

This page explains how the build scripts assemble those three layers, how `main.ts` orchestrates them at runtime, and what platform-specific quirks the code handles.

---

## How the pieces fit together

```text
┌──────────────────────────────────────────────────────┐
│  Electron shell  (app_dist/main.js)                  │
│                                                      │
│  ┌────────────────────┐  ┌────────────────────────┐  │
│  │  FastAPI sidecar   │  │  Next.js sidecar       │  │
│  │  resources/fastapi │  │  resources/nextjs      │  │
│  │  (PyInstaller bin) │  │  (standalone server.js)│  │
│  └────────────────────┘  └────────────────────────┘  │
│                                                      │
│  BrowserWindow → http://127.0.0.1:<nextjsPort>       │
└──────────────────────────────────────────────────────┘
```

Electron is the outer container — it creates the native window and provides the OS integration (single-instance lock, IPC, update checker). Both servers are spawned as ordinary child processes on dynamically chosen `localhost` ports. The `BrowserWindow` simply loads the Next.js URL once that server is ready.

---

## Build pipeline

The full build is orchestrated through npm scripts defined in `electron/package.json`. Running `npm run build:all` executes every step in order:

| Step | npm script | What it does |
|------|-----------|--------------|
| 1 | `clean:build` | Wipes `resources/nextjs`, `resources/fastapi`, and `app_dist` |
| 2 | `setup:env` | Installs Node deps for Electron, `uv sync` for FastAPI, and npm deps for Next.js |
| 3 | `build:ts` | Compiles `electron/app/**/*.ts` → `app_dist/` via `tsc` |
| 4 | `build:nextjs` | Builds the Next.js standalone bundle and copies it to `resources/nextjs` |
| 5 | `build:fastapi` | Runs PyInstaller via `uv run --with pyinstaller`, then copies static assets |
| 6 | `build:electron` | Generates version info, fetches the export runtime, type-checks, and calls `electron-builder` |

Sources: [electron/package.json:45-50]()

### Building the FastAPI bundle

The `build:fastapi` script runs PyInstaller inside `servers/fastapi` via `uv run --with pyinstaller python -m PyInstaller --distpath ../../electron/resources server.spec`. This produces a self-contained binary (no Python interpreter on the user's machine is needed). After PyInstaller finishes, `copy_fastapi_assets.js` copies two additional asset directories — `static/` and `assets/` — from the FastAPI source tree into `resources/fastapi/`:

```js
// electron/copy_fastapi_assets.js
const sources = [
  { name: "static", src: path.join(fastapiDir, "static"), dest: path.join(resourcesFastapiDir, "static") },
  { name: "assets", src: path.join(fastapiDir, "assets"), dest: path.join(resourcesFastapiDir, "assets") },
];
```

Sources: [electron/copy_fastapi_assets.js:7-10]()

### Building the Next.js bundle

`build_nextjs_resources.js` runs `npm run build` inside `servers/nextjs` with the environment variable `BUILD_TARGET=electron`, which tells Next.js to use its `output: "standalone"` mode. The resulting standalone directory (a self-contained Node.js server) is then copied wholesale into `resources/nextjs/`. The script handles a layout quirk in Next.js 16+: the standalone server runs from a nested `servers/nextjs/` path, so static files and the `public/` directory must be duplicated next to `server.js`:

```js
// Next.js 16 standalone traces the app under servers/nextjs/; the server process
// runs from that directory, so static assets and public files must live beside server.js
const nestedStandaloneDir = path.join(outDir, "servers", "nextjs")
```

Sources: [electron/build_nextjs_resources.js:57-61]()

### Packaging with electron-builder

`build.js` calls `electron-builder` directly with an inline configuration object. Key settings:

- `asar: false` — files are left unpacked so the FastAPI binary and Next.js `server.js` can be executed directly.
- `files: ["resources", "app_dist", "node_modules", "NOTICE"]` — only these four directories are bundled.
- `afterPack` hook — runs on macOS to `chmod 0o755` the FastAPI binary and any export converter binary, because zip extraction loses executable bits.

```js
// electron/build.js
const afterPack = async (context) => {
  if (context.electronPlatformName === "darwin") {
    fs.chmodSync(fastapiPath, 0o755)
  }
}
```

| Platform | Artifact format(s) |
|----------|--------------------|
| macOS    | `.dmg`             |
| Linux    | `AppImage`, `.deb` |
| Windows  | NSIS installer, `.appx` (Microsoft Store) |

Sources: [electron/build.js:6-58](), [electron/build.js:61-117]()

---

## Runtime: what happens when you launch the app

### 1. Single-instance lock

`main.ts` immediately calls `app.requestSingleInstanceLock()`. If another instance is already running, the new one quits; the existing window is focused instead.

Sources: [electron/app/main.ts:167-170]()

### 2. Path initialization

`initializeAppPaths()` resolves platform-appropriate directories for user data, temp files, logs, and cache before anything else happens. On macOS this is `~/Library/Application Support/Presenton Open Source`; on Linux it follows `XDG_CONFIG_HOME`; on Windows it uses `%APPDATA%`.

Sources: [electron/app/utils/constants.ts:234-292]()

### 3. Dependency check

Before showing the main UI, `checkDependenciesBeforeWindow()` verifies that LibreOffice, ImageMagick, and a bundled Chromium (for PDF export) are present. If any are missing, a setup window appears that installs them one after another. If the user cancels, the app quits.

Sources: [electron/app/main.ts:461-467]()

### 4. Port discovery and server startup

Two free `localhost` ports are found dynamically with `findUnusedPorts()`. Then `startServers()` launches both child processes sequentially — FastAPI first, Next.js second — and waits for each to become reachable via HTTP before proceeding.

```text
findUnusedPorts()
   → startFastApiServer(fastapiDir, fastApiPort, env)  → await fastApi.ready (polls /docs)
   → startNextJsServer(nextjsDir, nextjsPort, env)     → await nextjs.ready  (polls /)
   → mainWindow.loadURL(`http://127.0.0.1:${nextjsPort}`)
```

Sources: [electron/app/main.ts:551-575]()

### 5. Dev mode vs. packaged mode

`isDev` is `!app.isPackaged`. The two modes differ significantly in how servers are started:

| Mode | FastAPI command | Next.js command |
|------|----------------|-----------------|
| Dev | `uv run python server.py --port N --reload true` | `npm run dev -- -p N` |
| Packaged | `resources/fastapi/fastapi --port N` (binary) | `process.execPath server.js` (Node via Electron) |

In packaged mode, the Next.js standalone `server.js` is run using Electron's own Node.js runtime (`process.execPath`) with `ELECTRON_RUN_AS_NODE=1`, avoiding the need for a separate system Node installation.

Sources: [electron/app/utils/servers.ts:107-114](), [electron/app/utils/servers.ts:237-255]()

### 6. Environment injection

Before spawning either server, `startServers()` passes a large block of environment variables — LLM provider keys, image provider keys, feature flags, path overrides, and tool binary locations — directly into each child process's `env`. This is how user configuration (stored in `userConfig.json`) flows from the Electron main process down to the FastAPI and Next.js layers.

```ts
// electron/app/main.ts (excerpt, ~line 299-368)
const fastApi = await startFastApiServer(fastapiDir, fastApiPort, {
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
  OPENAI_API_KEY: process.env.OPENAI_API_KEY,
  OLLAMA_URL: process.env.OLLAMA_URL,
  // ... dozens more
}, isDev);
```

Sources: [electron/app/main.ts:299-370]()

### 7. Graceful shutdown

When the window closes or the OS signals a quit, `forceQuitApp()` runs a coordinated teardown: it stops any active export or LibreOffice install processes, then calls `stop()` on both the FastAPI and Next.js managed server objects. Each `stop()` sends `SIGTERM` to the child process and waits for it to exit before calling `app.exit()`.

Sources: [electron/app/main.ts:400-429]()

---

## Linux-specific workarounds

Two Linux issues are handled proactively at startup, before `app.whenReady()`:

1. **Chrome sandbox permissions** — The bundled `chrome-sandbox` binary must be root-owned with mode `4755` (setuid). If it is not, the app appends `--no-sandbox` to avoid a crash.
2. **Shared memory** — `/dev/shm` may be unavailable on some distributions, so `--disable-dev-shm-usage` is always appended on Linux to use `/tmp` instead.

Sources: [electron/app/main.ts:54-73]()

---

## Directory layout inside the installed package

```text
<app bundle>/
├── app_dist/          ← compiled Electron main process (TypeScript → JS)
│   └── main.js
├── resources/
│   ├── fastapi/       ← PyInstaller binary + static/ + assets/
│   │   └── fastapi    (or fastapi.exe on Windows)
│   ├── nextjs/        ← Next.js standalone server
│   │   ├── server.js
│   │   ├── .next-build/static/
│   │   └── public/
│   ├── export/        ← export runtime (Chromium, converter binary)
│   └── ui/            ← splash screen HTML and static images
└── node_modules/      ← Electron runtime dependencies
```

`asar: false` in `build.js` is what allows the OS to execute `resources/fastapi/fastapi` and `node resources/nextjs/server.js` directly; a packed `.asar` archive would make these binaries inaccessible to `spawn()`.

Sources: [electron/build.js:63-74]()

---

## Summary

The Electron wrapper's core job is coordination, not computation. At build time, three separate build tools — PyInstaller, Next.js standalone mode, and electron-builder — each package their own runtime into the `resources/` directory. At launch time, `main.ts` picks two free ports, injects configuration as environment variables, and races two child processes to readiness before revealing the UI. The arrangement is deliberately BYOK (bring your own keys): every LLM provider key is passed through as an environment variable, so no provider is hard-wired into the packaged binary.

Sources: [electron/app/utils/constants.ts:9-16](), [electron/app/main.ts:290-398]()
