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.
Contract overview
Section titled “Contract overview”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.
| Contract | Default alias | Typical providers | Typical consumers |
|---|---|---|---|
kix.contracts.postgresDatabase | "database" | cnpg-database, stackgres-database, kix.mkImport | firezone, harbor, immich, keycloak, metabase, n8n, plausible, vaultwarden |
kix.contracts.redis | "valkey" | valkey, kix.mkImport | harbor, immich, n8n |
kix.contracts.smtp | "smtp" | kix.mkImport | grafana, immich, keycloak, plausible, vaultwarden |
kix.contracts.oidc | "oidc" | oidc-client, kix.mkImport | grafana, harbor, immich |
kix.contracts.oidcProvider | "oidcProvider" | keycloak, kix.mkImport | oidc-client |
kix.contracts.gateway | "gateway" | envoy-gateway, traefik, kix.mkImport | kix.expose |
kix.contracts.otlpEndpoint | "otlp" | tempo, otel-collector, kix.mkImport | otel-collector |
Shared contract attributes and methods
Section titled “Shared contract attributes and methods”Each contract object in kix.contracts.* is constructed with mkContract in kix/lib/vocab/contract.nix and exposes:
| Attribute / method | Signature | Behavior |
|---|---|---|
alias | string | Default dependency argument name ("database", "valkey", "smtp", "oidc", "oidcProvider", "gateway", or "otlp"). |
fields | attrset | Map of field names to { required; shape; test; } definitions. |
check | who -> out -> out | Provider-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). |
problems | out -> [ 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. |
assertions | who -> 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). |
require | who -> dep -> dep | Eager 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:
384 root = {385 resource = credentials;386 # `postHooks` puts the Database, and the role and Secret it387 # requires, in this instance's own closure even when no consumer388 # reads `out.host`. `credentials` does not reference them and can be389 # applied first.390 postHooks = [ self.database ];391
392 out = kix.contracts.postgresDatabase.check who {393 host = self.pg.fqdn;394 port = self.pg.port;395 database = dbName;396 username = userName;397 extensions = config.extensions;398 inherit credentials;399 tls = postgres.out.tls or null;400 };401 };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):
109 deps.database = {110 # Metabase's first migration runs CREATE EXTENSION citext.111 config.extensions = [ "citext" ];112 assertions = dep: kix.contracts.postgresDatabase.assertions who dep;113 };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, orvalkey) builds its own Secret in the consumer’s namespace and publishes it atout.credentials. - External imports (
kix.mkImport): An external service declared withkix.mkImportpointsout.credentialsat asecretorsecret-refinstance in the same namespace usingcredentials = 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.
postgresDatabase fields
Section titled “postgresDatabase fields”| Field | Required | Type / shape | Description |
|---|---|---|---|
host | Yes | str | PostgreSQL hostname, such as a Service FQDN or an RDS / Cloud SQL endpoint. |
port | Yes | str | PostgreSQL port as a string (such as "5432"), kept as a string for URL and environment variable interpolation. |
database | Yes | str | Database name. |
credentials | Yes | secret (username, password) | Secret or secret instance exposing out.keyRef "username" and out.keyRef "password". |
username | No (required on provider check) | str | Database role name. Consumers read the role from out.credentials.out.keyRef "username", so a kix.mkImport does not need to repeat out.username. |
extensions | No (required on provider check) | list of str or { name; version; } | PostgreSQL extensions installed in the database. |
tls | No (required on provider check) | bool or null | Whether PostgreSQL serves TLS, or null when the provider cannot determine it. |
Importing an external PostgreSQL database
Section titled “Importing an external PostgreSQL database”217 instances.contracts-rds = {218 rds-login = {219 package = packages.secret-ref;220 config.keys = [221 "username"222 "password"223 ];224 };225
226 database.package = kix.mkImport {227 scope = "namespace";228 out = {229 host = "app.abc123.eu-west-1.rds.amazonaws.com";230 port = "5432";231 database = "metabase";232 tls = true;233 credentials = ref.contracts-rds.rds-login;234 };235 };236
237 encryption-key = {238 package = packages.secret-ref;239 aliases = [ "encryptionKey" ];240 config.keys = [ "key" ];241 };242
243 metabase.package = packages.metabase;244 };Redis (kix.contracts.redis)
Section titled “Redis (kix.contracts.redis)”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.
redis fields
Section titled “redis fields”| Field | Required | Type / shape | Description |
|---|---|---|---|
host | Yes | str | Server hostname, such as a Service FQDN or an ElastiCache endpoint. |
port | Yes | str of digits | Redis port as a string of digits (for example, "6379"). |
credentials | No (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. |
tls | No (required on provider check) | bool | Whether the server serves TLS (false on managed valkey). |
sentinel | No (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. |
redis helper functions
Section titled “redis helper functions”| Helper | Signature | Behavior |
|---|---|---|
kix.contracts.redis.credentialKeys | [ "password" ] | Required key names when out.credentials is present. |
kix.contracts.redis.usesSentinel | dep -> bool | Returns true when dep.out.sentinel is non-null. |
kix.contracts.redis.requirePlaintext | who -> dep -> dep | Throws 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). |
Importing an external Redis server
Section titled “Importing an external Redis server”248 instances.contracts-redis = {249 redis-login = {250 package = packages.secret-ref;251 config.keys = [ "password" ];252 };253
254 valkey.package = kix.mkImport {255 scope = "namespace";256 out = {257 host = "app-cache.abc123.euw1.cache.amazonaws.com";258 port = "6379";259 tls = true;260 credentials = ref.contracts-redis.redis-login;261 };262 };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).
smtp fields
Section titled “smtp fields”| Field | Required | Type / shape | Description |
|---|---|---|---|
host | Yes | str | SMTP server hostname or IP address (no whitespace, :, or /). |
port | Yes | str of digits or int (1..65535) | SMTP port number (for example, "587" or "465"). |
tls | Yes | "starttls" | "implicit" | "none" | Connection security mode. "none" is refused when external is true (or omitted). |
credentials | No | secret (username, password) | Optional secret or secret instance exposing out.keyRef "username" and out.keyRef "password". |
from | No | str (user@domain) or null | Default sender email address, overridden when the consuming instance sets config.mail.from. |
external | No | bool | true when the server is outside the cluster (the default assumed by consumer helpers when omitted), or false when it runs inside the cluster. |
smtp consumer helpers and options
Section titled “smtp consumer helpers and options”| Helper | Signature | Behavior |
|---|---|---|
kix.contracts.smtp.settings | { who, smtp, from ? null } -> attrset or null | Returns 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 null | Validates 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 null | Returns 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 attrset | Declares from (nullOr str containing @, default null) for options.mail = kix.options.mail;. |
Consumer mailer constraints
Section titled “Consumer mailer constraints”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:
plausibleandimmich: Requiretls = "implicit"wheneverout.credentialsis set, because their mailers send credentials in clear text if a server on astarttlsport does not offerSTARTTLS.grafana: Uses implicit TLS only on port 465, so it refusestls = "implicit"on any port other than 465 and refusestls = "starttls"or"none"on port 465.
Importing an SMTP server
Section titled “Importing an SMTP server”Without a login, one cluster-wide import can serve every namespace:
294 instances.mail.smtp.package = kix.mkImport {295 out = {296 host = "relay.internal.example.org";297 port = "587";298 tls = "starttls";299300 };301 };With a login, place the import in the consumer’s namespace beside its secret with scope = "namespace":
330 smtp-login = {331 package = packages.secret-ref;332 config.keys = [333 "username"334 "password"335 ];336 };337
338 smtp.package = kix.mkImport {339 scope = "namespace";340 out = {341 host = "smtp.mailgun.org";342 port = "465";343 tls = "implicit";344345 credentials = ref.vault.smtp-login;346 };347 };348
349 vaultwarden = {350 package = packages.vaultwarden;351 config = {352 domain = "https://vault.example.org";353354 };355 };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:
packages.oidc-client: Pairs a multi-realm identity provider (kix.contracts.oidcProvider, such askeycloak) with one realm name and one client secret (oidcClient).- A namespaced
kix.mkImport: Declares an external single-issuer provider directly in the application’s namespace withscope = "namespace"andcredentials = ref.<namespace>.<secret>.
oidc fields (kix.contracts.oidc)
Section titled “oidc fields (kix.contracts.oidc)”| Field | Required | Type / shape | Description |
|---|---|---|---|
issuerUrl | Yes | str (https?://...) | Expected iss claim in ID tokens and base URL for /.well-known/openid-configuration discovery. |
credentials | Yes | secret (clientId, clientSecret) | Secret or secret instance exposing out.keyRef "clientId" and out.keyRef "clientSecret". |
endpoints | No (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. |
oidc consumer helpers and options
Section titled “oidc consumer helpers and options”| Helper | Signature | Behavior |
|---|---|---|
kix.contracts.oidc.client | { who, oidc, options } -> attrset or null | Checks 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 attrset | Declares 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):
| Field | Required | Type / shape | Description |
|---|---|---|---|
issuerUrl | Yes | realm -> url | Function taking a realm name string and returning that realm’s issuer URL (https?://...). |
endpoints | No (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 tooidcProvider.out.issuerUrlandoidcProvider.out.endpoints.deps.oidcProvider(required): Explicitref.<namespace>.<provider>pointing to thekeycloakinstance (oroidcProviderimport).oidcClientdependency (required): Secret orsecret-refinstance in the same namespace declaringkeys = [ "clientId" "clientSecret" ].
389 grafana-oidc = {390 package = packages.secret-ref;391 aliases = [ "oidcClient" ];392 config.keys = [393 "clientId"394 "clientSecret"395 ];396 };397
398 oidc = {399 package = packages.oidc-client;400 deps.oidcProvider = ref.auth.keycloak;401 config.realm = "main";402 };403
404 grafana = {405 package = packages.grafana;406 config = {407 ingress.host = "grafana.example.org";408 oidc.autoLogin = true;409 };410 };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):
415 instances.photos = {416 immich-oidc = {417 package = packages.secret-ref;418 config.keys = [419 "clientId"420 "clientSecret"421 ];422 };423
424 oidc.package = kix.mkImport {425 scope = "namespace";426 out = {427 issuerUrl = "https://login.example.org";428 credentials = ref.photos.immich-oidc;429 };430 };431 };Gateway API (kix.contracts.gateway)
Section titled “Gateway API (kix.contracts.gateway)”kix.contracts.gateway (kix/lib/vocab/gateway.nix) defines the contract between Gateway API providers (envoy-gateway, traefik, or kix.mkImport) and kix.expose.
gateway fields
Section titled “gateway fields”| Field | Required | Type / shape | Description |
|---|---|---|---|
gatewayRef | Yes | { name; namespace; } | Name and namespace of the Kubernetes Gateway resource attached as parentRefs on generated HTTPRoute resources. |
listeners | No (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"). |
podSelector | No (required on provider check) | non-empty attrset of str, or null | Labels 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). |
podNamespace | No (required on provider check) | str or null | Namespace of the gateway data-plane pods. Must be set together with podSelector (or both must be null). |
gateway helper functions
Section titled “gateway helper functions”Unlike the other contracts, kix.contracts.gateway exposes require and claims instead of assertions:
kix.contracts.gateway.require who dep: Validatesdep.out(requiringgatewayRefand checking thatpodSelectorandpodNamespaceare either both set or bothnull) and returnsdep.kix.contracts.gateway.claims dep: Returnstruewhendepdeclares the"gateway"alias or publishesout.gatewayRef.kix.exposeuses this check so an unrelated application instance namedgatewayin 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).
otlpEndpoint fields
Section titled “otlpEndpoint fields”| Field | Required | Type / shape | Description |
|---|---|---|---|
otlpGrpcUrl | Yes | str (https?://host:port) | OTLP/gRPC base URL with no path (port 4317 by convention). |
otlpHttpUrl | Yes | str (https?://host:port) | OTLP/HTTP base URL with no path (port 4318 by convention; HTTP exporters append /v1/<signal>). |
tls | No (required on provider check) | bool or null | Whether the receivers serve TLS, or null when the provider cannot determine it. |
Package-family contracts (clickhouse)
Section titled “Package-family contracts (clickhouse)”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.