nixplan: sharing vibe coded software with friends
Every time I vibe code something nifty, the same thing happens. It runs beautifully on my machine, I want to hand it to my friends, and then reality arrives: one of them is on a different CPU architecture, and another runs a (potentially also vibe coded) software stack that looks nothing like mine. Building software for yourself has become easy. Making it run for other people, like your friends, is a whole different story.
There are attempts at solving this, most notably vibesdk from Cloudflare. But look at where it deploys: your software ends up on Cloudflare’s machines, as web software, because that is all their platform can run. If you want anything other than webtech, or anywhere other than their cloud, you’re out of luck. But the problem I care about is the other one: getting my software onto my friends’ machines. Those machines belong to them, not to me, so deploying really means creating a software package they install.
So recently I started LLM prototyping a deployment tool, one that is a bit too ambitious for its own good. Before this I worked on clan, a NixOS based deployment tool, so I had a large corpus of service definitions that were rather hacky because of design limitations in our system. I took the hackiest of those and wrote down what my ideal tool would have to support. This is the short list I ended up with:
- Everything about a service lives in its declaration.
- More than one operating system, more than one init system.
- A service can bring its own database or share one.
- Anything can be instantiated twice.
- Status
nixplan is a prototype in heavy machine-assisted development, and it is not for general use. Every code block below comes from the tree as it stands today, at most trimmed and renamed for reading.
Everything in one folder
First and foremost: service declarations are code, and everything is tied to a service declaration. Sharing a service with somebody means sending one folder, and reviewing it means reading one folder, and there is nothing else to look at. This is one of them, a borg backup client:
# modules/borg-push/client.nix
{ borgbackup, openssh, borgRepository }:
{ settings, ... }:
{
platforms = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
# The one thing it reads from another service: where the repository is.
uses.repo = {
interface = borgRepository;
reach = "one";
reads = [ "url" "quota" ];
};
impl =
{ results, ... }:
{
closure = [ borgbackup openssh ];
units.borgPush = {
command = "${borgbackup}/bin/borg create --stats ${results.repo.url}::{now} ${settings.path}";
env.BORG_QUOTA_GIB = toString results.repo.quota;
};
};
} As you can see, the platforms it runs on, the wire it reads and the unit it runs are all in the one file. None of it is hidden away in some machine config.
More than one OS, more than one init system
I want deployment to macOS and to embedded systems to be possible, which means the tool
cannot assume one operating system or one init system.
The service modules follow nixpkgs’ modular services,
so a module states the platforms it supports (the platforms list above) and every machine
states its own service manager. A machine that has no Nix at all gets a portable service image that carries everything the
service needs, which is what Rita’s Raspberry Pi in the diagram receives.
Two postgresqls at once
My friend Jonas’ game needs postgresql 11. His photo library wants 16. Most distributions and deployment systems support exactly one postgres per machine, so everything shares a global instance, and that gets hairy quick. Whether a service shares a database or brings its own has to be the deployment’s choice, not the packager’s. This deployment, adapted from a real end-to-end fixture, does both at once:
# instances.nix: one shared cluster, one consumer wired into it,
# and one service keeping a database of its own
{
instances = {
pg = {
module = cluster.services.default;
settings.cluster.databases.eu.owner = "game_eu";
placement.every.cluster = { tags = [ "database" ]; };
exposes = [ "eu" ];
};
game = {
module = game.services.default;
members.own.enable = false; # drop its bundled database
placement.every.client = { tags = [ "near" ]; };
wire.client.db = {
instance = "pg"; # and use the shared one
provides = "eu";
};
};
photos = {
module = photos.services.default; # same module shape, uncut
placement.every.client = { tags = [ "private" ]; };
placement.every.own = { tags = [ "private" ]; };
};
};
} As you can see, game and photos use the same module shape. One drops its bundled database and
wires into the shared cluster, and the other keeps its own postgres. That is requirement four for
free: instantiating a module twice is just writing its name twice, and the same works for two Tor
services or two copies of a game server.
Meet nixplan
The tool these requirements turned into is nixplan. Evaluating a deployment gives you a plan: one flat JSON file that says what runs where. No expressions, no derivations, nothing you need Nix to read. A second step then turns each plan entry into something the target machine can actually hold:
A machine that has its own Nix store gets a flakelet artifact: unit files plus metadata, and the service runs out of that store. A machine with no Nix at all gets a portable service image. Which one an entry gets is stated per entry, not per fleet, so Jonas and Rita can be served from the same deployment.
And when a deployment is broken, the planner does not throw a Nix stack trace at you. It still produces the plan, marks it as not applicable, and hands back a row that says how to fix it:
! greeter:greet@alpha greeter:greet@alpha declares the closure root `/opt/vendor/greeter`, which
is not a path under the store directory `/nix/store` the plan is read against
severity: error
evidence: a closure root is a literal path under the store the plan is read against, and a
consumer populates a filesystem from those roots
resolution: declare a path under `/nix/store` in `closure`, or plan the deployment against
the store `/opt/vendor/greeter` belongs to Summary
In this post I’ve written down where nixplan comes from: vibe coding made building software easy, sharing it with friends is still hard, and the existing answers deploy to a vendor’s cloud instead of to the machines your friends actually own. Four requirements from my clan days shaped the tool: everything in the service declaration, more than one OS and init system, databases shared or owned by choice, and everything instantiable twice.
So far this is proven on virtual machines in my own test harness and nowhere else. Real hardware I don’t own is the next step, I will keep you posted!