Architecture
The complete picture in one page. Detailed walkthroughs of each piece live in their own sections.
Block diagram
┌─────────────────────────────────────────────────────────────┐
│ glixos-core │
│ (project-owned flake) │
│ ├─ flake.nix # nixosModules.glixos, lib.import… │
│ ├─ modules/core/ # base system │
│ ├─ modules/desktop/ # optional desktop profile │
│ └─ lib/importManifest.nix │
└──────────────────────┬──────────────────────────────────────┘
│ flake input
▼
┌─────────────────────────────────────────────────────────────┐
│ user-packages flake │
│ (user-owned git repo, generated by `glix init`) │
│ ├─ flake.nix # glix-managed regions: │
│ │ # • inputs │
│ │ # • nixosConfigurations hosts │
│ ├─ hosts/<host>/ │
│ │ ├─ glix.toml # the ONLY file glix writes │
│ │ └─ default.nix # hardware/users; static after init │
│ ├─ shared/ # user-owned, free-form │
│ └─ flake.lock # pinned inputs │
└──────────────────────┬──────────────────────────────────────┘
│ nixos-rebuild switch --flake .#<host>
▼
Running system
Data flow for glix add
$ glix add --scope=home --user=alice github:owner/cool-pkg
┌── glix (Go) ──────── ─────────────────────────────────────┐
│ 1. resolveRepo() → repo.Repo{Root: ~/.config/glixos} │
│ 2. resolveRegistryURL() → registry URL or default │
│ 3. resolver.Resolve(ref, opts) │
│ - URI? → passthrough │
│ - registry? │
│ - nix registry list? │
│ returns Resolution{Ref, Source} │
│ 4. manifest.Load(hosts/laptop/glix.toml) │
│ 5. manifest.Package{Flake, Scope, User, ...} │
│ 6. repo.TakeSnapshot() ← in-memory state for rollback │
│ 7. manifest.Save() → glix.toml written atomically │
│ 8. flake.PatchFile(flake.nix, {inputs, hosts}) │
│ 9. nix flake lock │
│ on failure: snapshot.Restore() then return error │
│10. repo.Commit("glix add laptop: cool-pkg (...)") │
└──────────────────────────────────────────────────────────┘
Data flow for nixos-rebuild switch
nixos-rebuild --flake .#laptop switch
│
▼
flake.nix → outputs.nixosConfigurations.laptop
│
▼
mkHost "laptop" "x86_64-linux"
│
▼
let manifest = glixos-core.lib.importManifest {
manifestPath = ./hosts/laptop/glix.toml;
inputs;
defaultUser = "alice";
};
│ produces: { systemModules, homeModules, homeModulesByUser, manifest }
▼
nixosSystem {
modules = [
glixos-core.nixosModules.glixos # OS base
home-manager.nixosModules.home-manager
./hosts/laptop # hardware, users
] ++ manifest.systemModules; # per-package system modules
}
hosts/laptop/default.nix:
home-manager.users = lib.mapAttrs
(_: mods: { imports = mods ++ [ { home.stateVersion = "24.11"; } ]; })
manifest.homeModulesByUser; # per-user home-manager modules
Invariants
- Manifest is the truth. Re-running
nix flake lockfrom scratch must reproduce the sameflake.lockmodulo input timestamps, given the sameglix.toml. - Anchored regions are exclusive. Nothing outside
>>> glix-managed >>>…<<< glix-managed <<<is ever rewritten by glix. The patcher refuses to operate if a marker is missing. - One commit per mutation. If a command fails partway, snapshots restore state and no commit is created. If it succeeds, exactly one commit is created.
- Schema is additive. Adding a field with
omitemptydoes not bumpschema. A breaking change bumpsschemaand forces an explicit migration inmanifest.Load.
Where the boundaries are
| Boundary | Crossed by |
|---|---|
| Go ↔ Nix | manifest.Load ↔ builtins.fromTOML |
| glix ↔ user | The anchored regions; everything else is user-owned |
glixos-core ↔ user-packages | One flake input edge; no inverse |
glix.toml ↔ a package module | _module.args.glixConfig (string→string) |
glix ↔ nixos-rebuild | internal/nix/exec.go thin wrappers |
Next
- Codebase tour — what each package and file does.
- importManifest — the Nix function in detail.
- Patcher — anchored regions, how they work.
- Resolver — the URI → name resolution chain.
- Snapshots — how transactional rollback works.