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:
| Derived | 3 | fix/login |
|---|---|---|
| Directory | universes/3 | universes/fix-login |
| Slug, in hostnames and projects | u3 | fix-login |
| Default branch | spec.branch, u3 by default | the 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=mainA 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
| Command | What changes |
|---|---|
new | Worktrees, 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. |
up | Brings up the shared proxy, regenerates the routing, starts each app. Idempotent. |
down | Stops containers. Volumes and checkouts stay, so starting again is where you left off. |
rm | Destroys containers, volumes, worktrees and state. Asks first, and requires the universe's name typed back. |