Skip to content

Migrating from using clanModules to clanServices

Audience: This is a guide for people using clanModules. If you are a module author and need to migrate your modules please consult our new clanServices authoring guide

What's Changing?

Clan is transitioning from the legacy clanModules system to the clanServices system. This guide will help you migrate your service definitions from the old format (inventory.services) to the new format (inventory.instances).

Feature clanModules (Old) clanServices (New)
Module Class "nixos" "clan.service"
Inventory Key services instances
Module Source Static Composable via flakes
Custom Settings Loosely structured Strongly typed per-role
Migration Status Deprecated (to be removed) ✅ Preferred

Before: Old services Definition

services = {
    admin = {
        simple = {
            roles.default.tags = [ "all" ];

            roles.default.config = {
                allowedKeys = {
                    "key-1" = "ssh-ed25519 AAAA...0J jon@jon-os";
                };
            };
        };
    };
};

Complex Example: Multi-service Setup

# Old format
services = {
    borgbackup.production = {
        roles.server.machines = [ "backup-server" ];
        roles.server.config = {
            directory = "/var/backup/borg";
        };
        roles.client.tags = [ "backup" ];
        roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
    };

    zerotier.company-network = {
        roles.controller.machines = [ "network-controller" ];
        roles.moon.machines = [ "moon-1" "moon-2" ];
        roles.peer.tags = [ "nixos" ];
    };

    sshd.internal = {
        roles.server.tags = [ "nixos" ];
        roles.client.tags = [ "nixos" ];
        config.certificate.searchDomains = [
            "internal.example.com"
            "vpn.example.com"
        ];
    };
};

✅ After: New instances Definition with clanServices

instances = {
    # The instance_name is arbitrary but must be unique
    # We recommend to incorporate the module name in some kind to keep it clear
    admin-simple = {
        module = {
            name = "admin";
            input = "clan-core";
        };

        roles.default.tags."all" = {};

        # Move settings either into the desired role
        # In that case they effect all 'client-machines'
        roles.default.settings = {
            allowedKeys = {
                "key-1" = "ssh-ed25519 AAAA...0J jon@jon-os";
            };
        };
        # ----------------------------
        # OR move settings into the machine
        # then they affect only that single 'machine'
        roles.default.machines."jon".settings = {
            allowedKeys = {
                "key-1" = "ssh-ed25519 AAAA...0J jon@jon-os";
            };
        };
    };
};

Complex Example Migrated

# New format
instances = {
    borgbackup-production = {
        module = {
            name = "borgbackup";
            input = "clan-core";
        };
        roles.server.machines."backup-server" = { };
        roles.server.settings = {
            directory = "/var/backup/borg";
        };
        roles.client.tags.backup = { };
        roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
    };

    zerotier-company-network = {
        module = {
            name = "zerotier";
            input = "clan-core";
        };
        roles.controller.machines."network-controller" = { };
        roles.moon.machines."moon-1".settings = {
            stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
        };
        roles.moon.machines."moon-2".settings = {
            stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
        };
        roles.peer.tags.nixos = { };
    };

    sshd-internal = {
        module = {
            name = "sshd";
            input = "clan-core";
        };
        roles.server.tags.nixos = { };
        roles.client.tags.nixos = { };
        roles.client.settings = {
            certificate.searchDomains = [
                "internal.example.com"
                "vpn.example.com"
            ];
        };
    };
};

Steps to Migrate

Move services entries to instances

Check if a service that you use has been migrated In our reference

In your inventory, move it from:

services = { ... };

to:

instances = { ... };

Each nested service-instance-pair becomes a flat key, like borgbackup.simple → borgbackup-simple.


Add module.name and module.input

Each instance must declare the module name and flake input it comes from:

module = {
  name = "borgbackup";
  input = "clan-core";  # The name of your flake input
};

If you used clan-core as an input:

inputs.clan-core.url = "github:clan/clan-core";

Then refer to it as input = "clan-core".


Move role and machine config under roles

In the new system:

  • Use roles.<role>.machines.<hostname>.settings for machine-specific config.
  • Use roles.<role>.settings for role-wide config.
  • Remove: .config as a top-level attribute is removed.

Example:

roles.default.machines."test-inventory-machine".settings = {
  packages = [ "hello" ];
};

Important Type Changes

The new instances format uses attribute sets instead of lists for tags and machines:

# ❌ Old format (lists)
roles.client.tags = [ "backup" ];
roles.server.machines = [ "blob64" ];

# ✅ New format (attribute sets)
roles.client.tags.backup = { };
roles.server.machines.blob64 = { };

Handling Multiple Machines/Tags

When you need to assign multiple machines or tags to a role:

# ❌ Old format
roles.moon.machines = [ "eva" "eve" ];

# ✅ New format - each machine gets its own attribute
roles.moon.machines.eva = { };
roles.moon.machines.eve = { };

Migration Status of clanModules

The following table shows the migration status of each deprecated clanModule:

clanModule Migration Status Notes
admin Migrated
auto-upgrade ❌ Removed
borgbackup-static ❌ Removed
borgbackup Migrated
data-mesher Migrated
deltachat ❌ Removed
disk-id ❌ Removed
dyndns Being Migrated
ergochat ❌ Removed
garage Migrated
golem-provider ❌ Removed
heisenbridge ❌ Removed
importer Migrated
iwd ❌ Removed Use wifi service instead
localbackup Migrated
localsend ❌ Removed
machine-id ❌ Removed Now an option
matrix-synapse Migrated
moonlight ❌ Removed
mumble ❌ Removed
mycelium Migrated
nginx ❌ Removed
packages Migrated
postgresql ❌ Removed Now an option
root-password Migrated
single-disk ❌ Removed
sshd Migrated
state-version Migrated
static-hosts ❌ Removed
sunshine ❌ Removed
syncthing-static-peers ❌ Removed
syncthing Migrated
thelounge ❌ Removed
trusted-nix-caches Migrated
user-password Migrated
vaultwarden ❌ Removed
xfce ❌ Removed
zerotier-static-peers ❌ Removed
zerotier Migrated
zt-tcp-relay ❌ Removed

Warning

  • Old clanModules (class = "nixos") are deprecated and will be removed in the near future.
  • inventory.services is no longer recommended; use inventory.instances instead.
  • Module authors should begin exporting service modules under the clan.modules attribute of their flake.

Troubleshooting Common Migration Errors

Error: "not of type attribute set of (submodule)"

This error occurs when using lists instead of attribute sets for tags or machines:

error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.

Solution: Convert lists to attribute sets as shown in the "Important Type Changes" section above.

Error: "unsupported attribute module"

This error indicates the module structure is incorrect:

error: Module ':anon-4:anon-1' has an unsupported attribute `module'.

Solution: Ensure the module attribute has exactly two fields: name and input.

Error: "attribute 'pkgs' missing"

This suggests the instance configuration is trying to use imports incorrectly:

error: attribute 'pkgs' missing

Solution: Use the module = { name = "..."; input = "..."; } format instead of imports.

Removed Features

The following features from the old services format are no longer supported in instances:

  • Top-level config attribute (use roles.<role>.settings instead)
  • Direct module imports (use the module declaration instead)

extraModules Support

The extraModules attribute is still supported in the new instances format! The key change is how modules are specified:

Old format (string paths relative to clan root):

roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];

New format (NixOS modules):

# Direct module reference
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];

# Or using self
roles.client.extraModules = [ self.nixosModules.borgbackup ];

# Or inline module definition
roles.client.extraModules = [
  { config, ... }: {
    # Your module configuration here
  }
];

The extraModules now expects actual NixOS modules rather than string paths. This provides better type checking and more flexibility in how modules are specified.

Alternative: Using @clan/importer

For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated @clan/importer service:

instances = {
  my-importer = {
    module.name = "@clan/importer";
    module.input = "clan-core";
    roles.default.tags.my-tag = { };
    roles.default.extraModules = [ self.nixosModules.myModule ];
  };
};

Further reference