Skip to content
Install the CLI

Reference Package authoring

Package anatomy

A Kix package is a reusable blueprint for an application or cluster service. It declares the settings operators can customize (options), the other services it needs (build arguments), and the Kubernetes objects it creates (build). Reach for this page when you are writing a new package from scratch or adding typed configuration options to an existing one.

pkgs/echo/default.nix
{
scope,
lib,
kix,
availability,
...
}:
{
meta = {
version = "1.10.0";
description = "HTTP echo server for testing";
availability.max = "node";
};
options = {
image = kix.options.image {
repository = "hashicorp/http-echo";
tag = "1.0.0";
digest = "sha256:fcb74f691c8b0bd1ebff992c0a3ce2bf29f61ccb2f3d85fe72c4bd8b34e05061";
};
replicas = lib.mkOption {
type = lib.types.ints.positive;
default = kix.availability.pick availability {
none = 1;
pod = 2;
node = 3;
zone = 3;
};
description = "Number of echo server pods.";
};
resources = kix.options.resources {
requests = {
cpu = "50m";
memory = "64Mi";
};
limits.memory = "64Mi";
};
ingress = kix.options.ingress;
};
build = [
(
{ self, config }:
{
deployment = scope.mkDeployment {
name = "echo";
spec = {
replicas = config.replicas;
selector.matchLabels = scope.selectorLabels;
template.spec.containers = [
{
name = "echo";
image = kix.image.ref config.image;
ports = [ (kix.port "http" 5678) ];
resources = config.resources;
}
];
};
};
service = scope.mkResource (
kix.service.fromWorkload { name = "echo"; } self.deployment
);
root = self.service;
}
)
(kix.expose { service = "service"; })
];
}

A package file is either a Nix function taking system arguments or a plain attribute set containing a build attribute.

When written as a function, Kix calls it with four Phase 1 arguments before evaluating options:

Outer argumentTypeDescription
scopeattrsetInstance-scoped builders (mkResource, mkDeployment, mkSecret, mkCRD, forNamespace, selectorLabels, and related helpers).
libattrsetStandard nixpkgs.lib.
kixattrsetFramework helpers (kix.options, kix.availability, kix.image, kix.expose, kix.monitor, kix.mount, kix.contracts, and related helpers).
availabilitystringEffective availability level for the instance ("none", "pod", "node", or "zone"), clamped to meta.availability.max. Map it to replica counts or settings with kix.availability.pick availability { none = ...; pod = ...; node = ...; zone = ...; } and kix.availability.atLeast "<min>" availability.

Include ... in the outer function header ({ scope, lib, kix, ... }:) so the package accepts future framework arguments. Do not declare cross-package dependencies in the outer function header; declare them in build entries instead.

The package returns an attribute set with the following top-level keys:

AttributeTypeRequiredDescription
metaattrsetNoPackage metadata, platform role declarations, availability ceiling, compatibility bounds, and network intents.
optionsattrsetNoNixOS module option declarations (lib.mkOption or kix.options.*) exposed to instances.<name>.config.
configattrset or module functionNoPackage-level configuration module merged with options. Values inside config that the author wraps in lib.mkDefault are lowered from priority 1000 to 1250 (between option defaults at 1500 and instance or injected lib.mkDefault values at 1000).
depsattrsetNoPer-dependency configuration injection (config), warnings, and assertions keyed by build argument name.
buildfunction or list of functionsYesOne or more functions returning named resource parts and a root entry.
warnings{ config, self, deps }: [ string ]NoEvaluation-time warnings collected into cluster.warnings.
assertions{ config, self, deps }: [ { assertion; message; } ]NoEvaluation-time assertions checked before building manifests.

Across all build entries, the merged parts attribute set must define root. root designates the primary entry point of the package and determines what downstream packages receive when they depend on this instance.

root accepts either a direct resource or an extended attribute set:

# Direct resource
root = self.service;
# Extended root with extra dependencies, post-deploy hooks, and custom outputs
root = {
resource = self.service;
requires = [ self.migrationJob ];
postHooks = [ self.smokeTest ];
out = {
endpoint = "http://${self.service.out.fqdn}:8080";
};
};
root fieldTypeDefaultDescription
resourceresourceRequiredPrimary resource of the package. Must also be assigned to a named attribute in build (for example, service = ...; root = self.service;).
requireslist of resources[ ]Additional resources that must be ready before the package instance is considered ready.
postHookslist of resources[ ]Post-deployment resources (such as kix.healthCheck jobs) that run after root.resource and requires are ready.
outattrset{ }Extra fields merged onto root.resource.out and promoted to the top level of the dependency object passed to consumers.

Every resource defined in build must be reachable from root.resource, root.requires, or root.postHooks (either because root depends on it through .out string context or requires, or because it depends on a reachable resource such as an RBAC RoleBinding referencing a reachable ServiceAccount). Resources that are disconnected from root emit an orphaned-resource warning during evaluation.

root.requires and a resource’s own requires order different things. A resource’s requires becomes a kix.run/depends-on annotation on that resource, so deploy applies it after the listed resources are ready. root.requires extends the dependencies of the package’s PackageInstance, so consumers of the package wait for the listed resources, but nothing inside the package does. An operator that watches kinds its own package defines therefore puts the CRDs in the controller’s requires (requires = builtins.attrValues self.crds; on the mkDeployment call). With the CRDs only in root.requires, a first deploy can start the controller before the API server serves its kinds, and the controller crashes and restarts until it does. The scorecard rule architecture.controllerRequiresCrds warns about this shape.


meta declares static package properties that Kix inspects during dependency binding, cluster validation, and scorecard evaluation.

meta = {
version = "2.51.0";
description = "Prometheus monitoring system and time series database";
roles = [ "metricsApi" ];
defaultAliases = [ "monitoring" ];
availability.max = "node";
podSecurity = "restricted";
lifecycle = "stateful";
owner = "platform-observability";
annotationDomains = [ "monitoring.coreos.com" ];
compatibleWith.grafana = {
minVersion = "10.0.0";
reason = "Requires Grafana 10+ data source provisioning format";
};
k8s = {
minVersion = "1.28";
maxVersion = "1.32";
};
};
FieldTypeDefaultDescription
versionstringNoneUpstream software version ([vV]?<digits>(.<digits>)*[-<suffix>][+<build>]). Checked by meta.compatibleWith and recorded on the PackageInstance.
descriptionstringNoneNon-empty one-line summary of the package. Checked by the governance.hasDescription scorecard rule (info severity).
roleslist of strings[ ]Platform capabilities provided by this package.
defaultAliaseslist of strings[ ]Default dependency-injection names registered when instances.<name>.aliases is empty.
availability.max"none" | "pod" | "node" | "zone"NoneHighest availability level the package supports.
compatibleWithattrset of { minVersion?; maxVersion?; reason?; }{ }Version bounds for upstream dependencies keyed by dependency argument name.
k8s{ minVersion?; maxVersion?; reason?; }NoneSupported Kubernetes minor or patch version range, checked against k8sVersion.
conflictsWithlist of strings[ ]Packages that cannot coexist with this package ("<name>" for same-namespace, "cluster:<name>" for cluster-wide).
podSecurity"privileged" | "baseline" | "restricted"NonePod Security Standards enforcement level applied to the instance namespace.
unsafelist of strings[ ]Acknowledged security or architectural exceptions ("raw-pvc", "host-network", "privileged", "run-as-root", "cluster-admin").
annotationDomainslist of strings[ ]Annotation domain prefixes (such as "cert-manager.io") owned and consumed by this package.
networkattrset{ }Ambient network intents (intents.<part>.{egress,ingress}) and externalIngressEntities.
lifecycle"stateful"NoneMarks the PackageInstance with kix.run/lifecycle: stateful.
scope"cluster" | "namespace"unset"cluster": consumers in every namespace find an instance by name, and auto-instantiation places the package once per cluster (if a second namespace needs it from availablePackages, evaluation fails; declare one instance instead). "namespace": an instance is never used from another namespace, not even with ref. Unset: an instance in another namespace is used only when the consumer names it with deps.<arg> = ref.<namespace>.<instance>. Packages that claim a role or declare a cluster: conflict still serve the whole cluster, as do instances that Kix creates from availablePackages or enables via optional = true to satisfy a dependency. A kix.mkImport stand-in defaults to "cluster".
ownerstringNoneTeam or owner identifier used by governance.hasOwner and byOwner policy rules.
deprecated{ message; replacement?; }NoneEmits an evaluation warning when the package is instantiated.

Version strings must match <digits>(.<digits>)* with an optional leading v or V, an optional -<suffix>, and an optional +<build> (for example, "1.28.2", "v2.4.0-rc1", "1.31.2-gke.1209000", or "1.31.2+rke2r1").

When comparing versions in compatibleWith and k8s:

  • Numeric segments are compared as integers from left to right.
  • A -<suffix> whose first dot-separated identifier starts with alpha, beta, rc, pre, dev, or preview is treated as a pre-release and sorts before the corresponding release (1.0.0-rc1 < 1.0.0).
  • All other -<suffix> values (such as -r0, -debian-12, or -gke.1209000) and +<build> suffixes are treated as vendor build metadata and compare equal to the base version (1.0.0-debian-12 == 1.0.0).

meta.roles registers the instance as a provider of one or more cluster roles from a closed vocabulary:

RoleRequired out fieldsEffect
"cni"NoneSatisfies the cluster CNI capability check.
"dns"fqdnProvides cluster DNS resolution; workloads automatically receive a dependency and network egress rule to the DNS service.
"loadBalancer"NoneSatisfies Service resources of type = "LoadBalancer".
"metricsApi"NoneSatisfies consumers that call scope.requireMetricsApi (such as HorizontalPodAutoscaler).
"network-policy-enforcer"netpolEnables cluster-wide NetworkPolicy enforcement when cluster.networkPolicy.enable = true.
"registryMirror"NoneRegisters a cluster container registry mirror.
"snapshotController"crdsProvides the VolumeSnapshot CRD family for backup and restore workflows.
"storageClasses"storageClassNameProvides a dynamic StorageClass for PersistentVolumeClaim resources.

Each role in meta.roles also registers that role name as a dependency alias so other packages can request { dns ? null }: or { loadBalancer ? null }:. At most one active instance in a cluster may provide a given role unless disambiguated.

meta.defaultAliases provides additional dependency-injection names when the instance does not set instances.<name>.aliases. If an instance sets an aliases list, even an empty one, defaultAliases are replaced while meta.roles remain active.

meta.availability.max declares the highest availability topology the package architecture supports ("none", "pod", "node", or "zone"). When a cluster requests a higher level (for example, "zone"), Kix clamps availability inside the package to meta.availability.max so replica counts and topologySpreadConstraints never exceed what the software can safely run.

Individual workloads can further clamp their own ceiling by passing availability.max to scope.mkDeployment, scope.mkStatefulSet, scope.mkJob, or scope.mkCronJob.

meta.compatibleWith.<dep> checks the meta.version of the bound <dep> provider at evaluation time:

meta.compatibleWith.postgres = {
minVersion = "15";
maxVersion = "17";
reason = "Uses PostgreSQL 15+ logical replication syntax";
};

Bounds are inclusive (minVersion <= actual <= maxVersion) and compare only up to the segment precision of the bound. For example, maxVersion = "1.29" accepts "1.29.4", while maxVersion = "1.29.0" rejects "1.29.4". meta.k8s uses the same comparison rules against k8sVersion.

Entries in meta.conflictsWith prevent incompatible packages from being deployed together:

  • "<pkg>" forbids another instance of <pkg> in the same namespace.
  • "cluster:<pkg>" forbids <pkg> anywhere in the same cluster.
  • Listing the package’s own name in "cluster:<self>" enforces a cluster-wide singleton.

An instance counts when it answers <pkg> by its name or its aliases, or when its package lists <pkg> in meta.defaultAliases. The last case holds even when the instance sets its own aliases, so two instances of a singleton package fail whatever aliases they set.

Setting meta.podSecurity to "restricted", "baseline", or "privileged" applies the corresponding pod-security.kubernetes.io/enforce label to the generated Namespace resource. When multiple instances share a namespace, the least restrictive level wins (privileged > baseline > restricted).

meta.unsafe acknowledges intentional security or isolation trade-offs so the scorecard records them as reviewed grants (architecture.unsafeGrants) rather than unexpected violations:

meta.unsafe tokenWhat it permits
"raw-pvc"Constructing a raw PersistentVolumeClaim via scope.mkResource instead of a managed storage operator.
"host-network"Setting hostNetwork = true, hostPID = true, or hostIPC = true on a pod spec.
"privileged"Setting securityContext.privileged = true on a container.
"run-as-root"Running containers with runAsNonRoot = false or runAsUser = 0.
"cluster-admin"Binding a ServiceAccount to the built-in cluster-admin ClusterRole or granting wildcard * verbs and resources.

When cluster.networkPolicy.enable = true, Kix derives NetworkPolicy resources from the dependency graph. Workloads that need ambient network access not represented by a Kix resource dependency (such as scraping all pods or calling external webhooks) declare that access in meta.network:

meta.network = {
intents.deployment = {
egress = [
{
to = "apiserver";
}
{
to = "world";
ports = [ { port = 443; protocol = "TCP"; } ];
}
];
ingress = [
{
from = "all-pods";
ports = [ { port = 9090; protocol = "TCP"; } ];
}
];
};
externalIngressEntities = [ "world" ];
};
  • intents.<partName>.egress: list of rules where to is "world", "all-pods", "apiserver", "dns", or { entities = [ ... ]; }, with optional ports = [ { port; protocol ? "TCP"; } ].
  • intents.<partName>.ingress: list of rules where from is "world", "all-pods", or { entities = [ ... ]; }, with optional ports.
  • externalIngressEntities: list of Cilium entity names (such as ["world"] or ["remote-node"]) used when this package acts as a cluster ingress controller.

options defines the configuration schema for instances.<name>.config using standard NixOS module options (lib.mkOption) or kix.options.* helpers.

options = {
logLevel = lib.mkOption {
type = lib.types.enum [ "debug" "info" "warn" "error" ];
default = "info";
description = "Minimum log severity emitted by the server.";
};
extraEnv = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Additional environment variables passed to the main container.";
};
};

When Kix evaluates an instance, it merges configuration layers using the NixOS module system in the following priority order (lowest number wins):

  1. Option defaults (options.<name>.default): priority 1500.
  2. Package config defaults (lib.mkDefault values inside the top-level config block in the package file): lowered from priority 1000 to priority 1250 so package-level computed defaults override option defaults (1500) while yielding to dependency injections and instance config (including instance lib.mkDefault at 1000). Unwrapped definitions in the package config block keep normal module priority (100).
  3. Dependency config injections (deps.<arg>.config from downstream consumers) and instance config (instances.<name>.config): priority 100 (or lib.mkDefault at 1000, lib.mkForce at 50).

Both the package config block and instance config modules can take { config, lib, clusterConfig, ... }: as module arguments to compute values from sibling options or cluster settings.

Every package in a flake checked by kix.mkFlake or kix.checkPackageOptions must obey three rules on options defaults so users can override individual keys without unexpectedly wiping sibling defaults:

  1. No untyped options with non-empty attrset defaults. An option without a type (or typed as lib.types.anything / lib.types.raw) cannot have a non-empty attribute set as its default, because setting one subkey in instances.<name>.config replaces the entire attribute set. Use a submodule (lib.types.submodule) instead.
  2. No attrsOf or lazyAttrsOf with non-empty defaults. lib.types.attrsOf replaces the default map wholesale when a user sets a single key unless merged carefully, and prevents per-key schema documentation. Define known keys as explicit options or put user extensions in extra* options with default = { };.
  3. Submodule defaults must match field defaults. If a lib.types.submodule option has default = { foo = "bar"; };, the foo option inside that submodule must also declare default = "bar"; so specifying another field in the submodule does not drop foo.

kix.options.* provides pre-typed option schemas that pair directly with composition helpers like kix.expose, kix.monitor, and kix.healthCheck.

HelperArguments / shapeGenerated options and types
kix.options.image{ repository ? null, tag ? null, digest ? null, pullPolicy ? "IfNotPresent", tagText ? null, description ? ... }Submodule option with repository (str), tag (str), digest (nullOr str, defaults to digest while config.tag == tag, otherwise null), pullPolicy (nullOr str, default pullPolicy), plus pin record-keeping options sourceRev, promotedBy, resolvedAt, and note (all nullOr str, default null). Consumed by kix.image.ref.
kix.options.resources{ requests ? { }, limits ? { }, description ? ... }Submodule option with requests and limits (each supporting cpu, memory, and ephemeral-storage defaults plus freeform hugepages-<size> or extended <domain>/<name> quantities validated by kix.types.quantity; setting a field to null clears its default) and claims (nullOr (listOf (submodule { name; request ? null; }))). Cleaned via apply so it passes directly to a container’s resources field.
kix.options.envoption value (not a function)lib.mkOption of type attrsOf (nullOr envValue) with default = { }, where each value is a string, a { valueFrom = ...; } source, or null (to clear a package default). Consumed by kix.mergeEnv.
kix.options.ingressoption attrset (not a function)Attribute set of options { host; annotations; } where host is nullOr str (default null) and annotations is attrsOf str (default { }). Assign as options.ingress = kix.options.ingress;; read by kix.expose from config.ingress.
kix.options.monitoroption attrset (not a function)Attribute set of options { enabled; interval; } where enabled is bool (default true) and interval is nullOr str (default null). Assign as options.metrics = kix.options.monitor;; read by kix.monitor from config.metrics.
kix.options.alertsoption attrset (not a function)Attribute set { enabled; } where enabled is bool (default true), for packages that render alert rules without a kix.monitor scrape. Assign as options.metrics = kix.options.alerts;; read by kix.alerts from config.metrics.enabled.
kix.options.healthCheckoption attrset (not a function)Attribute set { enabled; retryFor; } where enabled is bool (default false) and retryFor is int (default 90). Assign as options.healthCheck = kix.options.healthCheck;; read by kix.healthCheck from config.healthCheck.
kix.options.oidc{ scopes, autoLogin ? null }Attribute set of options { scopes; autoLogin?; } where scopes is listOf str that must include "openid" (default scopes), and autoLogin is bool (default false, included only when autoLogin != null). Setting the removed enabled or realm options fails evaluation in kix.contracts.oidc.client with migration steps.
kix.options.mailoption attrset (not a function)Attribute set { from; } where from is nullOr str holding one @ (default null): the sender address that replaces the smtp server’s out.from. Assign as options.mail = kix.options.mail; and pass config.mail.from to kix.contracts.smtp.settings.

Resource quantities (kix.types.quantity and kix.parseQuantity)

Section titled “Resource quantities (kix.types.quantity and kix.parseQuantity)”

kix.types.quantity is a NixOS option type that accepts standard Kubernetes resource quantity strings (such as "100m", "0.5", "256Mi", "2Gi", or "1e3") and rejects malformed suffixes or numbers exceeding 64-bit signed integer limits or 18 significant digits.

To compare or compute resource quantities in Nix code, use kix.parseQuantity:

# Returns millicores (integer) when the first argument is "cpu"
cpuMillis = kix.parseQuantity "cpu" config.resources.requests.cpu; # "250m" -> 250, "2" -> 2000
# Returns bytes (integer) for memory or storage
memBytes = kix.parseQuantity "memory" config.resources.requests.memory; # "256Mi" -> 268435456

Packages often expose an escape hatch option (extraSpec, extraEnv, extraArgs, or settings) so cluster operators can set fields not covered by first-class options. Use Kix’s guard helpers to merge operator overrides while protecting invariants owned by the package:

HelperSignatureBehavior
kix.mergeGuarded{ who, locked, what ? "extraSpec" } base overridesRecursively merges overrides onto base, but throws an evaluation error naming who and what if overrides touches any attribute path (a list of path segments, such as [ [ "replicas" ] [ "selector" ] ] or [ [ "template" "spec" "serviceAccountName" ] ]) listed in locked, or replaces a parent of a locked path with a non-attrset value.
kix.mergeEnv{ who, owned ? [ ], defaults ? { } } userEnvValidates that userEnv does not set any environment variable claimed by owned (where each entry is { vars ? { }; names ? [ ]; from; fix; }, even as null), and returns the merged attribute set defaults // userEnv // <all owned.vars> ready to pass to kix.mkEnvVars.
kix.guardArgs{ who, option ? "extraArgs", owned } userArgsValidates CLI flags and returns userArgs unchanged. userArgs can be a list of flag strings (matching --flag, --flag=value, or a one-letter -f with an attached value) or an attrset keyed by flag name, like a VictoriaMetrics CR’s extraArgs (matching each key with or without leading dashes). owned maps each refused flag to its reason and fix; the error reads <who>: <option> sets "<flag>", which this package does not accept: <reason>.
# Guarding CLI flags and environment variables owned by typed options
args = [
"--storage.tsdb.retention.time=${config.retention}"
"--web.listen-address=:9090"
] ++ kix.guardArgs {
who = "prometheus";
owned = {
"--storage.tsdb.retention.time" = "set retention instead.";
"--web.listen-address" = "the Service and the probes use port 9090; remove it.";
};
} config.extraArgs;
env = kix.mkEnvVars (
kix.mergeEnv {
who = "myapp";
defaults = { LOG_FORMAT = "json"; };
owned = [
{
vars = {
PORT = "8080";
DATABASE_HOST = database.host;
};
from = "the package and its `database` dependency";
fix = "Wire the database with `deps.database` instead.";
}
];
} config.extraEnv
);

Each function in build is called via pattern matching on its attribute set parameter list. Kix recognizes the following built-in framework arguments, and treats any other argument name as a cross-package dependency request:

Built-in argumentDescription
selfLazy fixed point of the merged parts attribute set returned across all build entries. Use self.<partName> to reference sibling resources in the same package.
configEvaluated package options (options merged with package config, dependency injections, and instances.<name>.config).
clusterConfigEvaluated cluster module configuration (such as clusterConfig.cluster.domain, clusterConfig.cluster.name, clusterConfig.k8sVersion, clusterConfig.cluster.availability, and clusterConfig.networkPolicy.enable).
scopeInstance-scoped resource builders (identical to outer scope).
libStandard nixpkgs.lib.
kixFramework helpers.
depsAttribute set of all bound dependencies declared across build entries and deps.
availabilityEffective availability level string for the instance ("none", "pod", "node", or "zone").

Any argument to a build function outside the built-in list above requests another package instance by instance name, aliases, meta.defaultAliases, or meta.roles:

  • Required dependency ({ postgres, ... }: without ?): Kix requires a matching instance (or auto-instantiates the package when it is listed in the cluster’s availablePackages).
  • Optional dependency ({ prometheus ? null, ... }: with ? null): Bound to the provider’s output object if present in the cluster, or null if absent.

When a dependency is bound, the argument receives the provider’s consumer view:

  • dep.out: Full output attribute set (dep.root.resource.out // dep.root.out).
  • Top-level convenience fields: every key in dep.out is promoted directly onto dep (for example, postgres.fqdn and postgres.port), alongside any top-level part folders from the provider’s build (such as dep.crds and dep.secrets) and the view’s system keys (_type, _packageInstance, root, out, raw, and _meta). A root.out key or part folder that collides with a system key (or a part folder other than crds and secrets that collides with a root.out key) throws an evaluation error.
  • dep.crds: Attribute set of CRD resources from the provider’s crds part folder (unless overridden by root.out.crds).

The top-level deps attribute set lets a consumer package inject configuration into an upstream provider or attach dependency-specific assertions and warnings:

deps.postgres = {
config = consumerConfig: {
databases.${scope.instanceName} = {
owner = consumerConfig.dbUser;
};
};
assertions = pg: [
{
assertion = pg.out.tlsEnabled or false;
message = "myapp requires postgres to have TLS enabled.";
}
];
warnings = pg:
lib.optional (pg.out.maxConnections or 100 < 200)
"myapp recommends postgres maxConnections >= 200.";
};
deps.<arg> fieldTypeDescription
configattrset or consumerConfig: attrsetConfiguration module injected into the upstream provider instance. Throws an error if the provider instance’s config in the cluster definition locked the target option with kix.mkLocked.
assertionsdep: [ { assertion; message; } ]Assertions evaluated only when <arg> is non-null.
warningsdep: [ string ]Warnings evaluated only when <arg> is non-null.

Built-in dependency contracts (kix.contracts.*)

Section titled “Built-in dependency contracts (kix.contracts.*)”

kix.contracts.* provides shared contract validators (postgresDatabase, redis, smtp, oidc, oidcProvider, gateway, and otlpEndpoint) that verify the out surface passed between providers (including kix.mkImport stand-ins) and consumers:

  • Provider validation (check): A provider passes its root.out through kix.contracts.<name>.check who { ... }, which verifies every required and optional contract field and returns out.
  • Consumer validation (assertions or helper functions): A consumer checks a bound dependency in deps.<alias>.assertions with kix.contracts.<name>.assertions who (or reads it through a contract helper such as kix.contracts.smtp.settings, kix.contracts.smtp.server, kix.contracts.oidc.client, or kix.contracts.gateway.require), which checks all required contract fields and validates the shape of any present optional fields.
  • Credentials on contracts (out.credentials): Every contract that carries a login places the secret or secret instance at out.credentials (read via dep.out.credentials.out.keyRef "<key>") rather than taking a second secret dependency.
deps.database.assertions = kix.contracts.postgresDatabase.assertions "vaultwarden";

build can be a single function or a list of functions (or file paths to functions). When build is a list:

  1. Kix inspects the function arguments of every entry and takes the union of all dependency requests. If any entry declares a dependency without a default ({ redis }:), the dependency is required for the package even if another entry declares { redis ? null }:.
  2. Each entry is called with its requested arguments and returns an attribute set of named parts.
  3. Kix merges the returned attribute sets across all entries with mergeCollide. Any duplicate part name across build entries throws an evaluation error.

Parts, folders, and conditional null entries

Section titled “Parts, folders, and conditional null entries”

Each attribute in a build return set is one of the following:

  • A resource created by scope.mkResource, scope.mkDeployment, scope.mkSecret, scope.mkCRD, or a provider builder.
  • null, which Kix filters out automatically. Use if config.metrics.enabled then scope.mkResource { ... } else null or scope.mkOptional config.metrics.enabled (scope.mkResource { ... }) for conditional resources.
  • A nested folder (up to 3 levels deep) grouping related resources, such as rbac = { sa = ...; role = ...; binding = ...; };. Top-level folders are also exposed on the instance’s consumer view.
  • Known folders (secrets and crds):
    • Both secrets and crds must be attribute sets of resources rather than a single resource directly (assigning a resource directly to secrets or crds throws an evaluation error), and any entry inside them that is not a Kix resource (_type = "k8s-resource") emits an evaluation warning.
    • Place scope.mkSecret / scope.mkSecretRef resources under secrets = { ... }; and scope.mkCRD / scope.mkCRDRef resources under crds = { ... };. CRDs placed in crds are exposed to downstream consumers at dep.crds.<Kind>.

Because build accepts a list of functions, cross-cutting concerns are packaged as standalone build entries:

build = [
(
{ self, config }:
{
deployment = ...;
service = ...;
root = self.service;
}
)
(kix.expose { service = "service"; })
(kix.monitor { service = "service"; port = "metrics"; })
(kix.healthCheck {
runtime = kix.healthCheck.runtimes.shell;
script = ''
curl -sf "http://$SERVICE_FQDN:8080/healthz"
'';
env = { self, ... }: { SERVICE_FQDN = self.service.out.fqdn; };
})
];

See Composition helpers for the full reference on kix.expose, kix.monitor, kix.alerts, and kix.healthCheck.

Kix evaluates packages in its local store without running shell build scripts. Package and cluster definitions can evaluate expressions, read files from the repository (builtins.readFile, builtins.fromJSON), write files with builtins.toFile or builtins.path, and render Helm charts through scope.helm.fetchHelmChart and scope.helm.importChart.

Do not call pkgs.runCommand, pkgs.stdenv.mkDerivation, or pkgs.writeText inside a package or cluster definition; evaluation fails under the default kix builder.


A package can define top-level warnings and assertions functions. Both receive { config, self, deps } after options and build parts are wired. Because deps only contains bound dependencies, an absent optional dependency is omitted from deps (test for it with !(deps ? <name>) or (deps.<name> or null) == null):

warnings =
{ config, deps, ... }:
lib.optional (config.ingress.host != null && !(deps ? gateway) && !(deps ? ingressController))
"echo: ingress.host is set, but neither gateway nor ingressController is present in the cluster.";
assertions =
{ config, ... }:
[
{
assertion = !(config.tls.enable && config.tls.secretName == null);
message = "echo: config.tls.secretName must be set when config.tls.enable is true.";
}
];

Failed assertions abort evaluation immediately with all failed messages. warnings are collected into cluster.warnings and reported by kix check and kix build.

During package evaluation, Kix also emits automatic structural warnings for common wiring mistakes:

  • Orphaned resources: A part defined in build is not reachable from root.resource, root.requires, or root.postHooks.
  • Redundant requires: A resource lists a dependency in requires = [ ... ] that is already tracked implicitly through .out string context in its manifest.
  • Duplicate requires: The same resource appears more than once in a requires list.
  • Unordered postHooks: A resource in root.postHooks does not depend on root.resource (either directly or transitively).

kix.checkPackageOptions and kix.packageOptionViolations

Section titled “kix.checkPackageOptions and kix.packageOptionViolations”

kix.checkPackageOptions validates that a package’s options block obeys the option default contract across every availability level up to meta.availability.max:

kix.checkPackageOptions {
package = import ./pkgs/myapp;
who = "myapp";
}

kix.checkPackageOptions returns null when valid or throws an evaluation error listing all violations. To inspect the violations as data without throwing, call kix.packageOptionViolations { package = ...; who = "..."; }, which returns a list of violation attribute sets (empty when valid). kix.mkFlake runs kix.checkPackageOptions automatically across all exported packages.

When a package accepts an external Secret dependency ({ credentialsSecret ? null }:), wrap the dependency with kix.checkSecretKeys before reading its keys. kix.checkSecretKeys verifies that dep.out.keys declares all required keys and at least one complete key group in anyOf, then returns dep (or throws an evaluation error if any required keys are missing):

build =
{
self,
config,
credentialsSecret ? null,
...
}:
let
checkedSecret = kix.checkSecretKeys {
who = "myapp";
arg = "credentialsSecret";
dep = credentialsSecret;
keys = [ "username" ];
anyOf = [
[ "password" ]
[ "api-token" ]
];
};
in
{
...
};
FieldTypeDefaultDescription
whostringRequiredConsumer package name used in error messages.
argstringRequiredDependency argument name.
depresource or nullRequiredBound Secret dependency. Returned unchanged when dep is null or when all key requirements are satisfied (or if dep.out.keys is not set).
keyslist of strings[ ]Keys that must all be present in dep.out.keys.
anyOflist of lists of strings or nullnullWhen non-null, every key in at least one inner list ([ [ "a" "b" ] [ "c" ] ]) must be present in dep.out.keys.