Monday, 14 July 2026
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.
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.
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.
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.
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.
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).
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.
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.
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).
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.
Local filesystem access — extend the file tree to browse arbitrary directories
on the host machine using Node.js fs rather than the server's
JSON-backed project storage.
Remote projects in Electron — allow the Electron app to connect to a remote
Roject server (roject.rokojori.com) and list projects hosted
there alongside local ones. The JWT is already available; it's a matter of
pointing a request (or a BrowserView panel) at the remote URL with the token.