multiversectl GitHub

Universes

What a universe is made of, how it is named, and what it does and does not share with the others.

What a universe is made of

Creating one is four things on disk and nothing else:

  • A worktree of every repository, under <root>/universes/<id>/<repo>, checked out on the universe's branch. They are real directories you edit with your own editor and your own git.
  • A runtime project per app, named <workspace>-<slug>-<app>, which is what namespaces containers, networks and volumes.
  • A set of hostnames, one per host key, resolved when the universe is created and refreshed on every start.
  • A state file, universe.yaml, holding all of the above plus when it was created, when it last started and why it exists.

Nothing is copied that git already has: every worktree shares the clone's object store, so ten universes of a large repository cost ten checkouts, not ten histories.

Numbered or named

$ multiversectl new 3           # branch u3, hostnames api-u3.<domain>
$ multiversectl new fix/login   # branch fix/login, api-fix-login.<domain>

A universe is identified by the name you typed. Everything else about its identity is derived from it, so there is one place to look:

Derived3fix/login
Directoryuniverses/3universes/fix-login
Slug, in hostnames and projectsu3fix-login
Default branchspec.branch, u3 by defaultthe name itself

The slug is the name reduced to what DNS labels and runtime project names both accept, so feat/ARCA-123 is reachable at api-feat-arca-123.<domain> while the branch keeps its real name. A name whose slug would not fit in a DNS label is refused, with the limit named, rather than truncated into a collision with another universe.

Every command takes whichever of the three you happen to be looking at — the name, the directory or the slug — because all three are in front of you at different moments. up fix/login, up fix-login, up 3 and up u3 all find their universe, and an unknown name lists the ones that exist.

Branches

A universe checks out one branch per repository. Unless you say otherwise, a named universe uses its own name and a numbered one renders spec.branch:

spec:
  branch: dev/u{{universe}}   # default: u{{universe}}

--branch overrides it, repeatably: a bare name is every repository, repo=branch is one of them.

$ multiversectl new 2 --branch feat/invoicing --branch web=main

A branch that already exists locally is checked out as it is; one that exists only on the remote is checked out tracking it; one that does not exist yet is created from the remote's default branch, not the clone's own copy of it, so a universe never starts on code that is behind without saying so.

Removing a universe removes its worktrees but keeps the branches — uncommitted work is lost, committed work is not. Re-creating a universe of the same name therefore reuses the branch you already have.

What a universe remembers

A universe that has been sitting there for a week should still be identifiable, so its state file keeps more than the routing:

# ~/acme/universes/fix-login/universe.yaml
name: fix/login
id: fix-login
slug: fix-login
created: 2026-10-25T10:44:09Z
lastStarted: 2026-10-25T10:46:11Z
note: PR 412 review
manifest: /home/me/work/acme/multiverse.yaml
version: 0.0.1

Set the note when you create it, and ls shows it beside the name:

$ multiversectl new fix/login --note "PR 412 review"

ls -o wide adds the ages — created 3d ago ยท last started 2h ago — which is usually how you decide which universe to reclaim.

Lifecycle

CommandWhat changes
newWorktrees, secrets, state. Nothing starts. Safe to run again: existing checkouts are kept, and a repository added to the manifest since is added to the universe.
upBrings up the shared proxy, regenerates the routing, starts each app. Idempotent.
downStops containers. Volumes and checkouts stay, so starting again is where you left off.
rmDestroys containers, volumes, worktrees and state. Asks first, and requires the universe's name typed back.