Why Cradle Is No Longer a Desktop App: What Electron Costs a Solo Developer
Open Cradle started as an Electron desktop app. The whole engine lived inside it: node-llama-cpp for inference, better-sqlite3 and sqlite-vec for storage and vector search, operator triage with a two-layer risk assessment. The reasoning was straightforward: data never leaves the operator's machine, because the app is that machine.
That is no longer the case. Here is why I dropped Electron, what it was costing, and what replaced it.
What changed in the brief
The first version answered the question "how do I give an operator a local assistant". The current one answers a different question: how do I put a control layer between agents, data, tools and people. The formula the product is built around:
LLM proposes → Evidence proves → Ontology constrains → Policy permits → Cradle executes + records.
I am not building my own multi-agent framework. External tools make the agents; Cradle sets the boundaries: what an agent may propose, which proposals are permitted, what actually got executed and recorded.
An honest caveat about ontologies. They are an architectural direction, not a working feature. The verification engine exists; the verifier registry is still empty. If you read somewhere that Cradle "constrains actions through an ontology", that describes the goal, not the current state.
Once the brief changed, the desktop stopped following from it. A control layer is a server, not a window on a laptop.
What Electron cost
While the product was a desktop app, the overhead looked like the price of locality. For one developer it added up like this:
- macOS code signing and notarization. Every release meant 3–10 minutes waiting for the notary service, and now and then the process simply hung.
- The macOS runner in GitHub Actions is billed with a 10× minute multiplier. So the mac build was done locally, and the Windows NSIS installer in CI.
- Native modules rebuilt between ABIs. Tests run under Node, the app runs under Electron, and the two have different ABIs. better-sqlite3 and node-llama-cpp were rebuilt back and forth; the test script still starts with
pnpm rebuild better-sqlite3. - sqlite-vec ships platform binaries, one per OS and architecture.
- No hot reload in the main process. Any change to IPC or the engine meant restarting the app.
- Three platforms = three release pipelines, each with its own signing, its own installer and its own ways to fail.
None of these is fatal on its own. Together they ate time that one person at an early product stage does not have.
What replaced it
cradle-server is a headless process. It runs locally or on a GPU server. The UI is a web console: the same interface code that lived in the Electron renderer, now on top of an HttpBackend instead of the IPC bridge.
The key move is a registry of RPC handlers. A handler body is written once. On the desktop, bindRpcToIpc binds it to IPC; on the server, the POST /api/v1/rpc/:channel route serves it. Events that used to travel through webContents.send now go over SSE.
// One handler body, two transports.
registerRpc('agents:list', {
handler: (_args, ctx) => listAgents(ctx.projectId),
scopes: ['agents:read']
})
// Desktop: ipcMain.handle('agents:list', ...) — via bindRpcToIpc('agents')
// Server: POST /api/v1/rpc/agents:list — via registerRpcRoutes(server)
// Events: webContents.send(...) — now SSE on GET /api/v1/events
The route checks the key's scopes first and only then reveals whether the channel exists: an anonymous call always gets a 401, never a hint about the shape of the API.
A point's API key is stored only in the browser. The console keeps it in localStorage; the account holds just the point's name and URL. The server that hosts the console never sees the keys to your cradle-server instances.
Fully local mode
pnpm console:local
One command brings up the site with the console on localhost:3000 and cradle-server on :31416. No Firebase, no email, no external services: the login code is printed in the terminal, the admin API key in the server log on first start. This is the "everything on one machine" mode the desktop app began with, minus Electron.
Three kinds of points
The console settings switch between the points it connects to:
- Local machine — cradle-server on the laptop, GGUF models.
- Your own GPU server — the same cradle-server, moved onto hardware inside the perimeter.
- A test EC2 worker with Bedrock models — experiments only. A GPU instance in EC2 turned out too expensive, and Bedrock lets me run scenarios without my own hardware. For an air-gapped deployment this option is out by definition.
Limitations
- Electron is not yet removed from the repository. Forge, preload and the IPC bindings live alongside the server until the console reaches feature parity.
- A site served over https cannot call
http://<ip>: the browser blocks mixed content. Browsers allowhttp://localhost, but a remote point needs TLS. - Ontologies: see above. A direction, not a feature.
Sources
- Electron, Code Signing — signing and notarization requirements on macOS.
- Electron, Native Node Modules — why native modules must be rebuilt for Electron's ABI.
- GitHub Docs, About billing for GitHub Actions — minute multipliers for Windows and macOS runners.