IOSHI Architecture
Table of Contents
IOSHI separates a personal machine into three concerns. The point is not tidiness — it is that every question about the machine has one obvious place to look.
ioshi/
├── i-intelligence/ # the workspace: Emacs, compositor, theming
└── os-system/ # the substrate: base, firstboot
# hi-hardware/ — the machine and its network: not here
The distribution owns two of the three. The third is deliberately absent, and that absence is the most important thing on this page.
The layers are ordered by how often they change. The intelligence layer is edited constantly; the substrate rarely; the hardware layer, which lives in your flake rather than this one, almost never.
1. i — intelligence interface
The largest layer, and the one that owns the user's actual working day: Emacs (a pgtk build with a curated package set and modal editing), the EWM compositor, a terminal, the shell, theming, and around thirty Home Manager modules covering everything from git config to media tooling.
Modules here gate on feature flags, so the same layer produces a full desktop on one host and a terminal-only environment on another.
This layer is mostly Home Manager, but not exclusively. Its default.nix is
a Home Manager aggregate and importing it at system level fails to evaluate.
Where a system-level module is genuinely needed — ewm.nix is the case, since
a compositor needs a system service — it is exposed and imported
individually rather than by pulling in the directory.
The three concerns are descriptive, not enforced. They say what a piece of config is about, not which module system delivers it. Nothing checks the boundary and nothing is meant to.
2. os — operating system
Deliberately minimal. Two files:
- base — user and shell, timezone, locale, console keymap, Nix experimental
features, store optimisation, and garbage collection. Locale, timezone and
keymap are all
mkDefault, so a consumer overrides them without a fight. - firstboot — packages a one-time setup command, shellchecked at build time
so a broken script fails the build rather than the install. The distribution
owns the convention; you supply the script through
emanix.firstboot.text.
There is no desktop.nix and no server.nix. Audio, Bluetooth, printing,
containers and bootloaders are facts about a particular machine, so they belong
to the flake that knows which machine it is describing.
3. hi — hardware / internet
Not in this repository, on purpose.
Which drivers must be in the initrd, how the disks are partitioned, what mesh or network the host joins, which printer is in the room — these are facts about a machine. A distribution can know how to enable them. It cannot know which machines want them, so it should not decide.
This is the same argument that removed the roles, applied one level up. It is also why Tailscale is not here: a mesh VPN is worth having, but choosing one and wiring it to a particular coordination server is your decision, not the distribution's. Bring your own; NixOS makes it a few lines.
4. How a host is composed
lib/mkHost is the composer. It takes:
{ hostName, role, username, hardware ? null, extraModules ? [ ], homeModules ? [ ] }
and returns a nixosSystem built from the distribution core (emanix.nix),
the hostname, the username and role, the package overlay, the secret store and
Home Manager wiring — then your hardware module if you passed one, and
anything else in extraModules.
Three details are worth dwelling on.
The hostname is a default, not a fixed value. One platform cannot tolerate the
system setting its own hostname at activation: under WSL it breaks the user
session bootstrap. Making it mkDefault lets that host force it empty without
forking the composer.
role selects nothing. There were once profiles/roles/{workstation,server,wsl}.nix
and role chose between them. They were deleted on 2026-08-30, because by the
end they differed in almost nothing and what they carried was host shape rather
than distribution policy. role survives purely as metadata: it is recorded
on emanix.role, exported as EMANIX_ROLE by the shell, and interpreted by
whoever consumes it. A label, not a dispatch.
The two arguments mkHost was called with are not mkDefault. Everything else
the composer contributes is an opinion a consumer may outrank. username and
role are not opinions — they are the inputs. Overriding role to something
other than what mkHost was called with yields an incoherent host, with modules
branching on one answer while the system was composed under another. Role
output is an opinion; mkHost input is an invariant.
Use homeModules for the user's Home Manager config rather than reaching into
home-manager.users.<name> from extraModules. mkHost already knows the
username, and spelling it again in the consumer is how the two drift apart.
5. Why the split holds
The concerns cut along real seams:
- A new machine touches only your flake's hardware layer
- A new shape of machine touches only your flake — the distribution has one shape and needs no edit
- A new tool touches only
i-intelligence - The substrate is touched almost never, which is how you want it
When a change needs edits in two layers at once, that is a signal the boundary is wrong — not that the architecture is inconvenient.
See Adding a Host for the procedure, and Options for the flags the layers gate on.