Monday, 14 July 2026

Electron desktop app shell

Roject packaged as a standalone Windows desktop app with JWT-based auth, token persistence, Authorization header injection, and a local Express server running in-process.

What we built

Electron main process (electron/main.ts)

The Express server starts in-process inside Electron's main process via a compiled startServer(port) export. A BrowserWindow points at http://localhost:3000. The server's static file, data, and locale paths all previously relied on __dirname relative to the TypeScript source; a new source/server/rootDir.ts module resolves these using process.env.ROJECT_ROOT (set by Electron before starting the server) with a fallback that preserves the existing ts-node behaviour.

electron/main.ts startServer(port) source/server/rootDir.ts ROJECT_ROOT

Electron login window

A minimal electron/login.html form collects email and password. Credentials are sent to the main process via IPC (contextBridge + ipcRenderer.invoke). The main process calls POST https://account.rokojori.com/api/auth/login directly using Node.js https — no browser redirect, no CORS, no cookie. Tokens are written to userData/tokens.json and reloaded on the next app start.

electron/login.html electron/preload.ts contextBridge tokens.json

Authorization header injection

session.defaultSession.webRequest.onBeforeSendHeaders intercepts every request from the BrowserWindow to http://localhost:3000/* and adds Authorization: Bearer <accessToken>. The existing extractToken middleware already checked this header (designed for Electron from the start), so the frontend required zero changes.

Token refresh + navigation guard

Server-side 302 redirects to /api/auth/refresh-session are intercepted by will-redirect — the main process calls POST /api/auth/refresh with the refresh token, updates the stored tokens, and reloads the original URL. A will-navigate guard prevents the BrowserWindow from ever leaving localhost: any external navigation (e.g. the app's login link pointing to account.rokojori.com) is intercepted and replaced with the Electron login window.

Build pipeline additions

Three new tsconfigs: tsconfig.electron.json (compiles electron/build/electron/), tsconfig.electron-server.json (compiles source/server/build/server/ for Electron's in-process require). New npm scripts: electron:build, electron:dev, electron:dist. scripts/copy-electron-assets.js copies login.html into build/electron/. scripts/launch-electron.js spawns the Electron binary with ELECTRON_RUN_AS_NODE deleted from the environment (VS Code / Claude Code set this variable, which otherwise makes Electron behave as plain Node.js with no GUI or API).

electron-builder scripts/launch-electron.js ELECTRON_RUN_AS_NODE workaround

Key decisions

Express runs in-process, not as a child process

The outline originally described the server as a child process. Running it in-process is simpler, removes IPC overhead, and is equivalent for the first iteration. The compiled server JS is required at runtime via a dynamic require(serverPath) call.

ELECTRON_RUN_AS_NODE is set by the VS Code environment

When this env var is set, the Electron binary runs as plain Node.js: no GUI, no Electron API, require('electron') returns the binary path string. The launcher script deletes it before spawning so the binary initialises as a real Electron app. This was discovered by writing debug output to a log file from inside the Electron process.

rootDir.ts centralises all server path resolution

Six server files used path.join(__dirname, '..', '..', ...) relative to the TypeScript source depth. Compiled output is one level deeper (build/server/server/), breaking all paths. A single rootDir.ts module exposes ROOT that all files import, using ROJECT_ROOT when set (Electron) or the __dirname fallback (ts-node).

.env loaded by the Electron main process

The Express server reads JWT_SECRET from the environment. When started via npm start this is provided by the shell; when started from Electron there is no shell. A small inline loadEnv() function in electron/main.ts parses the project-root .env file and sets missing variables before startServer is called.

Workspace boards system

Task boards (workspace/boards/)

A new Boards section added to the workspace with three boards: Tasks (current sprint, To Do / In Progress / Done), Bugs (Critical and Visual/UI/UX lanes), Backlog (MVP / Nice To Have / To The Moon). Each board uses custom HTML elements — <task-item>, <task-title>, <task-content> — styled to match the workspace visual language (CSS variables, same spacing rhythm, lane headers matching section h2). Clicking a title toggles the content open/closed via hide-content class. Shared styles extracted to workspace/_assets_/boards.css.

workspace/boards/ task-item boards.css

Outline shortened

workspace/outline/index.html was rewritten to remove the long numbered to-do list and the "Smaller open improvements" bullet list. Those items were distributed across the three boards. The outline now covers only: what Roject is, what exists now (condensed into named cards), a one-card pointer to the boards, and the technical implementation reference. About half the original length.

Workspace index improved

workspace/index.html gained a Boards card and a "Quick orientation" section with separate guidance for humans and agents: read Outline → Guides → Reference before making changes; check Boards and the most recent History entry for current context.

What's next

See Tasks board for the current sprint. Local filesystem access and remote projects in Electron are the two active items.