Skip to content
Install the CLI

Reference Policy and runtime contracts

Dependency contracts

When an application package depends on a database, Redis cache, mail server, single sign-on provider, Gateway, or OpenTelemetry collector, Kix validates the connection details against a shared dependency contract (kix.contracts.*). Reach for this page when you are wiring an external service into a cluster with kix.mkImport, setting up oidc-client or smtp, or writing a package that provides or consumes one of these interfaces.

Every contract in kix.contracts.* defines the default dependency alias, the required and optional fields on dep.out, and validator functions for both providers and consumers.

ContractDefault aliasTypical providersTypical consumers
kix.contracts.postgresDatabase"database"cnpg-database, stackgres-database, kix.mkImportfirezone, harbor, immich, keycloak, metabase, n8n, plausible, vaultwarden
kix.contracts.redis"valkey"valkey, kix.mkImportharbor, immich, n8n
kix.contracts.smtp"smtp"kix.mkImportgrafana, immich, keycloak, plausible, vaultwarden
kix.contracts.oidc"oidc"oidc-client, kix.mkImportgrafana, harbor, immich
kix.contracts.oidcProvider"oidcProvider"keycloak, kix.mkImportoidc-client
kix.contracts.gateway"gateway"envoy-gateway, traefik, kix.mkImportkix.expose
kix.contracts.otlpEndpoint"otlp"tempo, otel-collector, kix.mkImportotel-collector

Each contract object in kix.contracts.* is constructed with mkContract in kix/lib/vocab/contract.nix and exposes:

Attribute / methodSignatureBehavior
aliasstringDefault dependency argument name ("database", "valkey", "smtp", "oidc", "oidcProvider", "gateway", or "otlp").
fieldsattrsetMap of field names to { required; shape; test; } definitions.
checkwho -> out -> outProvider-side validator. Verifies that root.out defines every field in fields (both required and optional) with a valid shape, and returns out (or throws an evaluation error naming who and the missing or malformed fields).
problemsout -> [ string ]Consumer-side problem finder. Checks that all required = true fields are present and valid, and validates the shape of any required = false fields that are present on out.
assertionswho -> dep -> [ { assertion; message; } ]Consumer-side assertion builder for deps.<alias>.assertions (exposed on all contracts except gateway, which exposes require instead). Because a kix.mkImport stand-in is checked as a consumer dependency, it only needs to set the required = true fields (plus any optional fields the consumer requires).
requirewho -> dep -> depEager consumer-side validator (exposed on kix.contracts.gateway and called internally by kix.contracts.smtp.server and kix.contracts.oidc.client). Validates dep.out with problems and returns dep, or throws an evaluation error.

In a provider package, pass root.out through check so Kix validates every required and optional field:

kix/packages/cnpg-database/default.nix
root = {
resource = credentials;
# `postHooks` puts the Database, and the role and Secret it
# requires, in this instance's own closure even when no consumer
# reads `out.host`. `credentials` does not reference them and can be
# applied first.
postHooks = [ self.database ];
out = kix.contracts.postgresDatabase.check who {
host = self.pg.fqdn;
port = self.pg.port;
database = dbName;
username = userName;
extensions = config.extensions;
inherit credentials;
tls = postgres.out.tls or null;
};
};

View source on GitHub ↗

In a consumer package, attach assertions to deps.<alias> so Kix validates the bound dependency (whether it is a managed provider or a kix.mkImport):

kix/packages/metabase/default.nix
deps.database = {
# Metabase's first migration runs CREATE EXTENSION citext.
config.extensions = [ "citext" ];
assertions = dep: kix.contracts.postgresDatabase.assertions who dep;
};

View source on GitHub ↗


Credentials in contracts (out.credentials)

Section titled “Credentials in contracts (out.credentials)”

Whenever a dependency contract carries a login, it places the login in out.credentials: a secret or secret-ref instance, or a Secret built by the provider package, whose out.keyRef reads the keys required by the contract. Every contract with a login defines out.credentials using credentialsField { keys; required ? true; } in kix/lib/vocab/contract.nix. A consumer package reads credentials directly from the dependency (dep.out.credentials.out.keyRef "<key>") rather than taking a separate secret dependency.

  • In-cluster providers: A managed provider (such as cnpg-database, stackgres-database, or valkey) builds its own Secret in the consumer’s namespace and publishes it at out.credentials.
  • External imports (kix.mkImport): An external service declared with kix.mkImport points out.credentials at a secret or secret-ref instance in the same namespace using credentials = ref.<namespace>.<instance>.

Because a Kubernetes pod can only read Secrets from its own namespace, and Kix rejects cross-namespace Secret references at build time, any kix.mkImport that sets out.credentials must be placed in the consumer’s namespace beside its secret with scope = "namespace". An import without a login can keep the default scope = "cluster" and serve every namespace. When several namespaces connect to the same external service with their own secrets, use a Nix let binding to share the common host and port fields across the per-namespace imports.


PostgreSQL database (kix.contracts.postgresDatabase)

Section titled “PostgreSQL database (kix.contracts.postgresDatabase)”

kix.contracts.postgresDatabase (kix/lib/vocab/postgres-database.nix) defines the contract for packages that answer the database alias.

FieldRequiredType / shapeDescription
hostYesstrPostgreSQL hostname, such as a Service FQDN or an RDS / Cloud SQL endpoint.
portYesstrPostgreSQL port as a string (such as "5432"), kept as a string for URL and environment variable interpolation.
databaseYesstrDatabase name.
credentialsYessecret (username, password)Secret or secret instance exposing out.keyRef "username" and out.keyRef "password".
usernameNo (required on provider check)strDatabase role name. Consumers read the role from out.credentials.out.keyRef "username", so a kix.mkImport does not need to repeat out.username.
extensionsNo (required on provider check)list of str or { name; version; }PostgreSQL extensions installed in the database.
tlsNo (required on provider check)bool or nullWhether PostgreSQL serves TLS, or null when the provider cannot determine it.
clusters/test-doc-how-tos.nix
instances.contracts-rds = {
rds-login = {
package = packages.secret-ref;
config.keys = [
"username"
"password"
];
};
database.package = kix.mkImport {
scope = "namespace";
out = {
host = "app.abc123.eu-west-1.rds.amazonaws.com";
port = "5432";
database = "metabase";
tls = true;
credentials = ref.contracts-rds.rds-login;
};
};
encryption-key = {
package = packages.secret-ref;
aliases = [ "encryptionKey" ];
config.keys = [ "key" ];
};
metabase.package = packages.metabase;
};

View source on GitHub ↗


kix.contracts.redis (kix/lib/vocab/redis.nix) defines the contract for servers speaking the Redis protocol. valkey provides it in-cluster, and consumers (harbor, immich, n8n) request it as their valkey dependency.

FieldRequiredType / shapeDescription
hostYesstrServer hostname, such as a Service FQDN or an ElastiCache endpoint.
portYesstr of digitsRedis port as a string of digits (for example, "6379").
credentialsNo (required on provider check)secret (password)Secret or secret instance exposing out.keyRef "password". Omit on a kix.mkImport when the external server takes no password.
tlsNo (required on provider check)boolWhether the server serves TLS (false on managed valkey).
sentinelNo (required on provider check)null or { port; primarySet; }When Redis Sentinel fronts the server, an attribute set with port (string of digits, such as "26379") and primarySet (string, such as "primary"). null when Sentinel is disabled.
HelperSignatureBehavior
kix.contracts.redis.credentialKeys[ "password" ]Required key names when out.credentials is present.
kix.contracts.redis.usesSentineldep -> boolReturns true when dep.out.sentinel is non-null.
kix.contracts.redis.requirePlaintextwho -> dep -> depThrows an evaluation error if dep.out.tls is true, and returns dep otherwise. Used by harbor and immich, whose Redis clients in the package do not connect over TLS (n8n supports tls = true).
clusters/test-doc-how-tos.nix
instances.contracts-redis = {
redis-login = {
package = packages.secret-ref;
config.keys = [ "password" ];
};
valkey.package = kix.mkImport {
scope = "namespace";
out = {
host = "app-cache.abc123.euw1.cache.amazonaws.com";
port = "6379";
tls = true;
credentials = ref.contracts-redis.redis-login;
};
};

View source on GitHub ↗


Mail (kix.contracts.smtp and kix.options.mail)

Section titled “Mail (kix.contracts.smtp and kix.options.mail)”

kix.contracts.smtp (kix/lib/vocab/smtp.nix) defines the contract for the optional smtp ? null dependency taken by packages that send email (grafana, immich, keycloak, plausible, and vaultwarden).

FieldRequiredType / shapeDescription
hostYesstrSMTP server hostname or IP address (no whitespace, :, or /).
portYesstr of digits or int (1..65535)SMTP port number (for example, "587" or "465").
tlsYes"starttls" | "implicit" | "none"Connection security mode. "none" is refused when external is true (or omitted).
credentialsNosecret (username, password)Optional secret or secret instance exposing out.keyRef "username" and out.keyRef "password".
fromNostr (user@domain) or nullDefault sender email address, overridden when the consuming instance sets config.mail.from.
externalNobooltrue when the server is outside the cluster (the default assumed by consumer helpers when omitted), or false when it runs inside the cluster.
HelperSignatureBehavior
kix.contracts.smtp.settings{ who, smtp, from ? null } -> attrset or nullReturns null when smtp is null. Otherwise validates smtp and returns { host; port; portNumber; tls; external; from; credentials; }, where port is a string, portNumber is an integer, from is the from argument (if non-null) or smtp.out.from, and credentials is smtp.out.credentials or null. Refuses tls = "none" when external is true, and refuses a server when both from and smtp.out.from are null.
kix.contracts.smtp.server{ who, smtp } -> attrset or nullValidates smtp with the same rules and return shape as settings, except from may be null. Used by packages such as keycloak that configure sender addresses inside application realms rather than at the container level.
kix.contracts.smtp.egressPolicy{ scope, name, selector, smtp } -> resource or nullReturns a network policy opening TCP egress on smtp.portNumber to "world" for pods matching selector, or null when smtp is null or smtp.external is false.
kix.contracts.smtp.credentialKeys[ "username" "password" ]Required key names when out.credentials is present.
kix.options.mail (kix.contracts.smtp.options)option attrsetDeclares from (nullOr str containing @, default null) for options.mail = kix.options.mail;.

When smtp is wired, consuming packages refuse overlapping SMTP keys in env or settings and check that tls and port match their mailer’s behavior:

  • plausible and immich: Require tls = "implicit" whenever out.credentials is set, because their mailers send credentials in clear text if a server on a starttls port does not offer STARTTLS.
  • grafana: Uses implicit TLS only on port 465, so it refuses tls = "implicit" on any port other than 465 and refuses tls = "starttls" or "none" on port 465.

Without a login, one cluster-wide import can serve every namespace:

clusters/test-doc-how-tos.nix
instances.mail.smtp.package = kix.mkImport {
out = {
host = "relay.internal.example.org";
port = "587";
tls = "starttls";
};
};

View source on GitHub ↗

With a login, place the import in the consumer’s namespace beside its secret with scope = "namespace":

clusters/test-doc-how-tos.nix
smtp-login = {
package = packages.secret-ref;
config.keys = [
"username"
"password"
];
};
smtp.package = kix.mkImport {
scope = "namespace";
out = {
host = "smtp.mailgun.org";
port = "465";
tls = "implicit";
credentials = ref.vault.smtp-login;
};
};
vaultwarden = {
package = packages.vaultwarden;
config = {
domain = "https://vault.example.org";
mail.from = "[email protected]";
};
};

View source on GitHub ↗


Single sign-on (kix.contracts.oidc and kix.contracts.oidcProvider)

Section titled “Single sign-on (kix.contracts.oidc and kix.contracts.oidcProvider)”

Applications that sign users in with OpenID Connect (grafana, harbor, and immich) take an optional oidc ? null dependency checked against kix.contracts.oidc (kix/lib/vocab/oidc.nix). Single sign-on is enabled whenever oidc is bound.

Two kinds of instance satisfy oidc, both placed in the application’s namespace beside the client secret:

  1. packages.oidc-client: Pairs a multi-realm identity provider (kix.contracts.oidcProvider, such as keycloak) with one realm name and one client secret (oidcClient).
  2. A namespaced kix.mkImport: Declares an external single-issuer provider directly in the application’s namespace with scope = "namespace" and credentials = ref.<namespace>.<secret>.
FieldRequiredType / shapeDescription
issuerUrlYesstr (https?://...)Expected iss claim in ID tokens and base URL for /.well-known/openid-configuration discovery.
credentialsYessecret (clientId, clientSecret)Secret or secret instance exposing out.keyRef "clientId" and out.keyRef "clientSecret".
endpointsNo (required by consumers that do not use OIDC discovery, such as grafana){ authorizationUrl; tokenUrl; userinfoUrl; }Explicit OAuth 2.0 / OIDC endpoint URLs, each an http:// or https:// URL string.
HelperSignatureBehavior
kix.contracts.oidc.client{ who, oidc, options } -> attrset or nullChecks options (config.oidc) and throws a migration error if the removed enabled or realm options are set. Returns null when oidc is null, or { issuerUrl; endpoints; credentials; } when bound (evaluating .endpoints throws an error if the provider did not publish out.endpoints).
kix.options.oidc (kix.contracts.oidc.clientOptions){ scopes, autoLogin ? null } -> option attrsetDeclares scopes (listOf str that must include "openid", defaulting to scopes) and, when autoLogin != null, autoLogin (bool, default false).
kix.contracts.oidc.credentialKeys[ "clientId" "clientSecret" ]Required key names on out.credentials.
kix.contracts.oidc.endpointKeys[ "authorizationUrl" "tokenUrl" "userinfoUrl" ]Required attribute names when out.endpoints is set.

Multi-realm providers (kix.contracts.oidcProvider and packages.oidc-client)

Section titled “Multi-realm providers (kix.contracts.oidcProvider and packages.oidc-client)”

A multi-realm identity provider such as keycloak publishes functions of the realm name on root.out, checked by kix.contracts.oidcProvider (kix/lib/vocab/oidc-provider.nix):

FieldRequiredType / shapeDescription
issuerUrlYesrealm -> urlFunction taking a realm name string and returning that realm’s issuer URL (https?://...).
endpointsNo (required on provider check)realm -> { authorizationUrl; tokenUrl; userinfoUrl; }Function taking a realm name string and returning that realm’s authorization, token, and userinfo URLs.

packages.oidc-client (meta.scope = "namespace", meta.defaultAliases = [ "oidc" ]) connects a consumer namespace to an oidcProvider:

  • config.realm (str, required): Realm name passed to oidcProvider.out.issuerUrl and oidcProvider.out.endpoints.
  • deps.oidcProvider (required): Explicit ref.<namespace>.<provider> pointing to the keycloak instance (or oidcProvider import).
  • oidcClient dependency (required): Secret or secret-ref instance in the same namespace declaring keys = [ "clientId" "clientSecret" ].
clusters/test-doc-how-tos.nix
grafana-oidc = {
package = packages.secret-ref;
aliases = [ "oidcClient" ];
config.keys = [
"clientId"
"clientSecret"
];
};
oidc = {
package = packages.oidc-client;
deps.oidcProvider = ref.auth.keycloak;
config.realm = "main";
};
grafana = {
package = packages.grafana;
config = {
ingress.host = "grafana.example.org";
oidc.autoLogin = true;
};
};

View source on GitHub ↗

Importing a single-issuer external OIDC provider

Section titled “Importing a single-issuer external OIDC provider”

When the external identity provider has a single issuer URL, declare a kix.mkImport in the application’s namespace with scope = "namespace" (add out.endpoints when the consumer, such as grafana, requires explicit endpoints):

clusters/test-doc-how-tos.nix
instances.photos = {
immich-oidc = {
package = packages.secret-ref;
config.keys = [
"clientId"
"clientSecret"
];
};
oidc.package = kix.mkImport {
scope = "namespace";
out = {
issuerUrl = "https://login.example.org";
credentials = ref.photos.immich-oidc;
};
};
};

View source on GitHub ↗


kix.contracts.gateway (kix/lib/vocab/gateway.nix) defines the contract between Gateway API providers (envoy-gateway, traefik, or kix.mkImport) and kix.expose.

FieldRequiredType / shapeDescription
gatewayRefYes{ name; namespace; }Name and namespace of the Kubernetes Gateway resource attached as parentRefs on generated HTTPRoute resources.
listenersNo (required on provider check and when reading exposure.url)list of { name; port; protocol; }Gateway listener definitions (port is an integer, protocol is "HTTP" or "HTTPS").
podSelectorNo (required on provider check)non-empty attrset of str, or nullLabels of the gateway data-plane pods so kix.expose can generate an ingress NetworkPolicy on exposed backends. Must be set together with podNamespace (or both must be null).
podNamespaceNo (required on provider check)str or nullNamespace of the gateway data-plane pods. Must be set together with podSelector (or both must be null).

Unlike the other contracts, kix.contracts.gateway exposes require and claims instead of assertions:

  • kix.contracts.gateway.require who dep: Validates dep.out (requiring gatewayRef and checking that podSelector and podNamespace are either both set or both null) and returns dep.
  • kix.contracts.gateway.claims dep: Returns true when dep declares the "gateway" alias or publishes out.gatewayRef. kix.expose uses this check so an unrelated application instance named gateway in the same namespace is not mistaken for a Gateway API controller.

OpenTelemetry (kix.contracts.otlpEndpoint)

Section titled “OpenTelemetry (kix.contracts.otlpEndpoint)”

kix.contracts.otlpEndpoint (kix/lib/vocab/otlp-endpoint.nix) defines the contract for OpenTelemetry receivers that answer the otlp alias (tempo, otel-collector, or kix.mkImport).

FieldRequiredType / shapeDescription
otlpGrpcUrlYesstr (https?://host:port)OTLP/gRPC base URL with no path (port 4317 by convention).
otlpHttpUrlYesstr (https?://host:port)OTLP/HTTP base URL with no path (port 4318 by convention; HTTP exporters append /v1/<signal>).
tlsNo (required on provider check)bool or nullWhether the receivers serve TLS, or null when the provider cannot determine it.

In addition to the framework-wide kix.contracts.* vocabulary, the ClickHouse package family shares an internal contract at kix/packages/clickhouse/contract.nix (default alias "clickhouse"), implemented by both single-node clickhouse and operator-managed clickhouse-cluster and consumed by clickhouse-database. It requires httpUrl, nativeEndpoint, host, ports ({ http; native; }), username, credentials (a secret with key "password"), clusterName, version ("YY.M"), replicas, and replicaHosts.