Across the other projects — the NHS job watcher, the port-detection API, client sites — I kept needing the same thing: build it, test it, deploy it. GitHub Actions was the default choice, and its free tier, 2,000 minutes a month even on the generous side, sounded like plenty.
The challenge: stop burning through GitHub's free CI minutes — especially on Electron builds, which are slow — without paying per minute for compute I mostly already had sitting idle.
Why an Electron build alone can eat a free tier
Packaging a desktop app with electron-builder isn't a quick job on a hosted runner: installing dependencies from scratch, compiling native modules, producing separate installers per OS — Windows and Linux, at minimum. Do that for one app, then add the linting, tests, and deploy steps every other project also needed, and the minutes disappeared well before the month did.
The real question: why pay for a runner at all?
None of those steps actually needed an ephemeral cloud machine — they needed a machine, and I already had servers and computers sitting mostly idle. The thing missing wasn't compute. It was something to watch for a trigger, connect to a server, and run the steps in order. That's a much smaller problem than "CI/CD platform," and it's the one CID actually solves.
It was never only about not paying GitHub, either. Running it myself meant the run logs and the secrets behind a deploy stayed on infrastructure I actually controlled, instead of living inside someone else's platform — and it left room to shape access the way a real team needs it, rather than settling for whatever a third-party CI product happened to offer.
Deciding what CID would and wouldn't do
A hosted service that clones repos and runs builds for you has to stay online, hold everyone's SSH keys, and be paid for somehow — exactly the cost I was trying to remove. So the actual execution moved to a desktop app running on my own machine instead: CID-app, built with Electron and React. It reads the local git repo directly (via simple-git), connects to target servers over SSH/SFTP (ssh2, with SOCKS5 proxy support for anything behind a bastion host), and runs each workflow's steps there.

CID-api — Express and MongoDB — only stores projects, workflow definitions, encrypted secrets and keys, and run history. It never touches your code or your servers. That split is what keeps the whole thing free to run: the API is small and cheap to host, and the compute that used to cost GitHub Actions minutes now happens on hardware that was already mine.

Triggers without a server to receive them
No public-facing server means no webhooks to receive. The push trigger works by having CID-app poll the local git folder roughly every 15 seconds and compare the latest commit against the last one it saw. The cron trigger is node-cron running inside the app process itself, with no separate scheduler service. Both trade instant reactions for something free and simple to run.
That's a real limitation, not an oversight: a schedule only fires while CID-app is open on some machine. It's the same shape of problem the NHS job watcher hit with needing a browser tab open — solved differently here, but not eliminated. It's this project's own price for staying free.
Handling other people's SSH keys and secrets
Once this stopped being just my own pipeline — CID shipped as a real product with its own sign-up — other people's servers and secrets were suddenly involved, and the trust bar changed completely. SSH private keys and project secrets are encrypted at rest (secrets with AES-256-GCM), decrypted only for the moment a connection actually opens, and the API is never in a position to see your code. A self-hosted CID-api option exists for anyone unwilling to trust the hosted one at all with even the config.
What a run actually looks like
A run starts with a Sync files phase over SFTP, uploading changed files straight to the server's deploy path — no git access needed on the target server at all, no deploy keys to manage there. Then each step's shell command runs over SSH from that path, in order, with the project's environment variables and secrets exported first. Logs stream live into the run panel as SSH output arrives; if a step fails, the run stops and everything after it is marked skipped.

Where it landed
CID now runs CI/CD for pumpkins.dev's other projects — including the Electron builds for CID-app itself, on real hardware instead of a metered runner. It shipped as its own product rather than staying an internal tool, distributed as a Windows installer through CID-releases, with self-hosting the API still on the table for anyone who wants it.
What I'd take from this
- "Free CI minutes" are really just someone else's machine, rented by the minute — if you already own a machine, the cheaper fix is orchestration, not chasing a bigger free tier.
- Having no public server to receive events is a real constraint, not a free lunch: polling instead of webhooks, in-process cron instead of a scheduler service. Accept the tradeoff explicitly instead of pretending it isn't there.
- The moment a tool starts holding someone else's credentials, "it works on my machine" security stops being good enough — encrypt at rest, decrypt only at the point of use, and leave people a self-hosted way out.