Hello there. First, I must say that I am just an ordinary NixOS user, not a module maintainer, and certainly not an experienced expert user, so my perspective may therefore be highly one-sided. And second, I must clarify that the title is not intended to disparage the hard work of the module maintainers. For the reasons behind this statement, please read on.
Today, I spent several hours investigating exactly what purpose stateVersion serves. I even attempted to use hands-on experimentation to circumvent the effects of stateVersion, though ultimately without success. Now, I would like to write an article to serve as a record of this experience.
So in short, based on my limited study of nixpkgs and home-manager source code (They are very large repos after all), stateVersion is basically the patch of default.
Here are some examples from nixpkgs:
# 1. from `./nixos/modules/tasks/swraid.nix`
enable_implicitly_for_old_state_versions = lib.versionOlder config.system.stateVersion "23.11";
# 2. from `./nixos/modules/services/x11/desktop-managers/xterm.nix`
services.xserver.desktopManager.xterm.enable = mkOption {
type = types.bool;
default = versionOlder config.system.stateVersion "19.09" && xSessionEnabled;
defaultText = literalExpression ''versionOlder config.system.stateVersion "19.09" && config.services.xserver.enable;'';
description = "Enable a xterm terminal as a desktop manager.";
};
# 3. from `./nixos/modules/services/databases/postgresql.nix`
services.postgresql.package =
let
mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
mkWarn =
ver:
warn ''
The postgresql package is not pinned and selected automatically by
`system.stateVersion`. Right now this is `pkgs.postgresql_${ver}`, the
oldest postgresql version available and thus the next that will be
removed when EOL on the next stable cycle.
See also https://endoflife.date/postgresql
'';
base =
# XXX Don't forget to keep `defaultText` of `services.postgresql.package` up to date!
if versionAtLeast config.system.stateVersion "25.11" then
pkgs.postgresql_17
else if versionAtLeast config.system.stateVersion "24.11" then
pkgs.postgresql_16
else if versionAtLeast config.system.stateVersion "23.11" then
pkgs.postgresql_15
else if versionAtLeast config.system.stateVersion "22.05" then
pkgs.postgresql_14
else if versionAtLeast config.system.stateVersion "21.11" then
mkThrow "13"
else if versionAtLeast config.system.stateVersion "20.03" then
mkThrow "11"
else if versionAtLeast config.system.stateVersion "17.09" then
mkThrow "9_6"
else
mkThrow "9_5";
in
# Note: when changing the default, make it conditional on
# âsystem.stateVersionâ to maintain compatibility with existing
# systems!
mkDefault (if cfg.enableJIT then base.withJIT else base);
};
Other examples from home-manager:
# 1. from `./modules/programs/zsh/default.nix`
dotDir = mkOption {
default =
if config.xdg.enable && lib.versionAtLeast config.home.stateVersion "26.05" then
"${config.xdg.configHome}/zsh"
else
homeDir;
defaultText = lib.literalExpression ''
if config.xdg.enable && lib.versionAtLeast config.home.stateVersion "26.05" then
"''${config.xdg.configHome}/zsh"
else
config.home.homeDirectory
'';
example = literalExpression ''"''${config.xdg.configHome}/zsh"'';
description = ''
Directory where the zsh configuration and more should be located,
relative to the users home directory. The default is the home
directory.
'';
type = types.nullOr types.str;
};
# 2. from `./modules/home-environment.nix`
home.username = lib.mkIf (lib.versionOlder config.home.stateVersion "20.09") (
lib.mkDefault (builtins.getEnv "USER")
);
home.homeDirectory = lib.mkIf (lib.versionOlder config.home.stateVersion "20.09") (
lib.mkDefault (builtins.getEnv "HOME")
);
As we can see, the use of these stateVersions is largely due to changes in the default values of options across versions; some features are enabled by default on older NixOS installations but disabled on newer ones, while others are the exact opposite. Certain specialized packages (such as databases) use default versions based on the current stateVersion.
In short, regardless of what stateVersion affects, their purpose is essentially the same: module maintainers use these stateVersions for compatibility. To maintain compatibility with historical default values, they ensure that older configurations do not become invalid with nixpkgs updates, preventing stateVersion from silently corrupting user data⌠Itâs all about compatibility. Since itâs all about maintaining compatibility with past default values, the default values are, ultimately, the root of it all.
But, is it worth it? To set the default value, we need stateVersion as an anchor, which effectively introduces time into Nixâs evaluation process. In other words, the final build result isnât determined solely by the userâs configuration, but rather by the userâs configuration combined with the time at which the user installed NixOS. You might say, âBut isnât stateVersion part of the configuration?â Iâd say yes, but also no. The reason is that while stateVersion is indeed part of the configuration and can be changed by the user, both the wiki and expert users advise against doing so, because âThe consequences of changing its value range from none at all, to complete destruction of data written by specific software.â Herein lies the problem: the impact of stateVersion is beyond the userâs control. While stateVersion determines the default behavior, these defaults are set solely by âmodule maintainersâ and remain opaque to users unless they are willing to dig through the source code. So, even though stateVersion is part of the userâs configuration, it influences aspects the user is completely unaware of, is this really a good thing?
Furthermore, I believe the best practice we can identify is that there is no such thing as a best practice. What seems like a best practice today may not be appropriate in the future, which is why the default values set by module maintainers are subject to change. When default values change, some module maintainers choose to quietly include if statements to maintain compatibility, while others do the same but also print numerous warnings to inform users of the change and explain that they must configure the module themselves to make the warnings disappear.
My view is that changes made for the sake of compatibility can make modules increasingly difficult to maintain. Module maintainers are forced to write numerous if statements whenever a default value changes, and they have to rewrite them every time the default changes. This not only makes maintenance difficult but also deters potential maintainers who want to understand the module, while increasing the burden on the current maintainer.
Now letâs consider what this results in. Perhaps itâs user-friendliness, users have fewer things to think about; they simply need to enable a certain option. As for what that option actually does⌠they donât fully know (unless they consult the source code). This means that if users want to be in full control, they will have to read the source code, because they donât know exactly what an option does behind the scenes. If users want to customize their configuration from scratch and ensure NixOS doesnât do anything they havenât explicitly specified, theyâll need to examine a vast amount of source code. But that seems quite difficult, because no one knows exactly what these modules in the massive repository are doing behind the scenes⌠everyone just knows that they work.
So my thought is, maybe we should give the choice back to the users? Let them decide what they want to enable, which database version to use, and whether to configure Nvim with Lua or Viml. The modules should no longer interfere with usersâ decisions, but simply inform them (through documentation or other means) of what they can do and what the consequences will be, leaving everything up to the user. Iâm not saying this approach is necessarily better than the default settings, because NixOS isnât âsmartâ anymore, users will be forced to learn this and that. This might drive away new users and require them to write more configuration files. And even if we were to proceed with this, such a drastic change could not be implemented overnight. Such an update would cause most NixOS builds to fail, which would undoubtedly cause an uproar. But, to all you hardworking module maintainers out there, do you really think itâs a good idea to let the scope of stateVersion continue to expand?
These are just my humble thoughts. Please donât hesitate to share your ideas and critiques.