I have been using Cordis as the runtime layer for an Electron application that installs software from a marketplace.
The packages in this marketplace are closer to applications than browser extensions. They can ship a frontend, a backend, background workers, local storage, native binaries, or whatever else the application needs. I still want the host to control installation, permissions, updates, and cleanup.
Cordis fits in the middle. It gives each installed package a lifecycle and a context for accessing services from the host.
Applications as plugins
An installed application follows a package format that the host understands:
example/
├── manifest.json
├── frontend/
├── backend/
├── assets/
└── scripts/
The frontend runs inside Electron. Cordis represents the package inside the host process and manages the services it registers or consumes.
A plugin can receive host services through its context:
ctx.plugin(application)
ctx.service('storage', storage)
ctx.service('processes', processManager)
ctx.service('permissions', permissions)
I can keep APIs for storage, notifications, configuration, process management, and other host features in one place. The application only receives the services it needs.
Cordis also gives those services a lifecycle. When I unload a plugin, Cordis can dispose of the effects and registrations that belong to its context. I do not have to leave process cleanup and subscriptions scattered across the application.
Keeping the backend independent
Electron should not decide which language an application uses on the server side.
A package can use Node.js, Python, Rust, Go, or a native executable. The manifest tells the host what to start for the current platform:
{
"backend": {
"windows": "./backend/windows/server.exe",
"linux": "./backend/linux/server",
"darwin": "./backend/darwin/server"
}
}
The host starts the process and connects it to the application through a small RPC layer. Cordis manages the plugin around that process.
That split keeps the Cordis side small. It does not need to understand Python environments or Rust binaries. The process manager handles those details.
Why I stopped using VMs
I first tried to make the runtime accept almost any application without repackaging it for the host.
The setup used Linux environments under the desktop client, with containers and virtual machines providing the dependencies an application expected. It worked, but the memory cost became hard to justify.
A VM consumes RAM for the guest operating system before the application inside it does useful work. Running several installed applications meant keeping too much runtime infrastructure in memory next to Electron, the user's browser, IDE, and everything else on the machine.
Containers reduced some of that overhead. On Windows and macOS, I still needed a Linux VM underneath the setup I wanted.
The marketplace already controls how applications are distributed, so I moved the compatibility work into the package instead. A package can ship separate binaries for each supported platform, or declare that it only supports one.
backend/
├── windows/
│ └── server.exe
├── linux/
│ └── server
└── darwin/
└── server
That removed the need to keep a guest operating system alive for each application while preserving the part I cared about: different applications can still use different technology stacks.
Runtime installation
Installing a package now follows a predictable path:
Download package
↓
Read manifest
↓
Install package dependencies
↓
Register Cordis plugin
↓
Start backend process
↓
Mount frontend
Uninstalling it gives the host the same information in reverse. It knows which plugin context to dispose, which backend process to stop, and which files belong to the application.
The package contract also makes permissions easier to handle. An application can declare what it needs before the host starts it instead of receiving broad access to the machine through a VM.
Editable installs
I want installed applications to stay editable.
You should be able to open one in an editor, change the frontend or backend, and run it again without rebuilding the desktop client. A coding agent should be able to work on the same source tree.
application/
├── manifest.json
├── AGENTS.md
├── frontend/
│ └── src/
└── backend/
└── src/
The host knows how to restart the backend and reload the frontend because the manifest defines those entry points. Cordis already owns the plugin lifecycle, so development mode uses the same load and unload path as a normal installation.
The current split
The runtime now has a small set of responsibilities.
Cordis manages plugins, shared services, dependencies, and lifecycle. The Electron host manages installation, permissions, frontend mounting, and external processes. Each package owns its application code and declares which platforms it supports.
That gives me the application model I wanted without keeping a collection of virtual machines running in the background.