knext

Operator & the NextApp CRD

The Go operator is the single source of truth for cluster state.

knext follows one hard rule: nothing mutates the cluster out-of-band. A single Go operator watches NextApp resources and reconciles each into a running Knative Service. The CLI only ever applies the CR — it never touches Knative or infra manifests directly.

kn-next deploy   →  build · push · emit CR
NextApp CR       →  applied to the cluster
operator         →  reconciles desired state
Knative Service  →  scaled-to-zero, reachable

The NextApp resource

  • apiVersion: apps.kn-next.dev/v1alpha1
  • kind: NextApp

One resource describes the whole deployment. The image field is required and must be digest-pinned.

nextapp.yaml
apiVersion: apps.kn-next.dev/v1alpha1
kind: NextApp
metadata:
  name: acme
spec:
  image: registry/acme@sha256:9f1c...   # required, digest-pinned
  scaling:
    minScale: 0
    maxScale: 20
    containerConcurrency: 100
  cache:
    enableBytecodeCache: true                  # DEPRECATED — the compile cache is now baked into
                                               # the image by default; you no longer need this
  storage:
    provider: gcs
    bucket: acme-assets
  security:
    networkPolicy: {}                          # default-on internal-only isolation
status:
  url: https://acme.apps.example.com
  conditions: []                               # Ready / Degraded / Reconciling

Spec fields (selected)

FieldNotes
imageRequired. Container image — must be digest-pinned (@sha256:).
scalingminScale, maxScale, containerConcurrency.
resourcescpuRequest, memoryRequest, cpuLimit, memoryLimit. Each must be a positive Kubernetes quantity (250m, 512Mi, 1Gi), at most 64 characters, and — if it uses scientific notation — with an exponent no larger than ±9999. A request may not exceed its own limit.
storageprovider, bucket, region, endpoint.
cacheprovider, url, keyPrefix configure the data cache (ISR/data). enableBytecodeCache, bytecodeCacheSize are deprecated — the boot compile cache is now baked into the image by default, so you no longer need to enable them; they still work but will be removed in a future release. See bytecode caching.
revalidationqueue, kafkaBrokerUrl, provisionKafkaSource — async ISR revalidation (see below).
envPlain, non-secret env vars (name → value). Reserved names are rejected at admission (see below).
secretsenvFrom, envMap (Kubernetes Secret refs) — see Binding secrets.
databaseBind a bring-your-own Postgres by referencing an existing Secret (secretRef / optional roSecretRef). knext does not provision databases. See Databases.
runtimebun or node (default node). A Bun bytecode-built image only boots under Bun — flipping it to node makes the pod exit with a loud FATAL message; rebuild instead. See Bun runtime.
securitynetworkPolicy — pod-level L3/L4 isolation, default-on.
trafficrevisionName, canaryPercent (rollback / canary).
buildIdNext.js BUILD_ID, stamped as label apps.kn-next.dev/build-id (skew protection).

Plain env vars (spec.env)

spec.env sets non-secret environment variables — feature flags and runtime tuning. Values are stored verbatim in the resource, so anything sensitive belongs in spec.secrets instead.

spec:
  env:
    FEATURE_FLAG_BETA: "on"
  • Names must be valid env identifiers ([A-Za-z_][A-Za-z0-9_]*).
  • The reserved names HOSTNAME, PORT, K_SERVICE, K_REVISION, and K_CONFIGURATION are rejected when you apply the resource — they are managed by the platform, and overriding them would break request routing.
  • If an env name collides with a platform-managed variable or a secrets.envMap mapping, the env entry is ignored and a Warning event is recorded on the resource (kubectl describe nextapp <name>) explaining which side won.

Binding secrets: envMap + DATABASE_URL

spec.secrets is the CR's mechanism for secret-backed environment variables: envFrom injects every key of a Secret; envMap maps a single env var to a specific Secret key. The canonical use is the one-Secret database binding — the app reads DATABASE_URL from the environment, and the CR binds it from a Kubernetes Secret:

nextapp.yaml (database binding)
spec:
  secrets:
    envMap:
      DATABASE_URL:
        secretName: acme-db-app
        secretKey: pooler-url

This works with any Postgres the Secret points at — a CloudNativePG cluster (point the URL's host at the pooler Service, not the primary, so scale-to-zero fan-out doesn't storm the database), or a managed/serverless provider with a scale-to-zero gateway. Swapping providers is a Secret change, zero app or CR schema change. knext binds the Secret; it does not create it or provision the database.

Databases: spec.database

knext is engine-agnostic — it does not provision or manage a database. spec.database is a bring-your-own binding: it wires an existing Postgres Secret into the app as DATABASE_URL.

secretRef binds a Secret in the app's own namespace as DATABASE_URL; roSecretRef optionally binds a read-only DSN as DATABASE_URL_RO (its key defaults to DATABASE_URL_RO, so one Secret carrying both keys binds with the same name). It is typed sugar over the envMap path above — same injection machinery, same semantics. kn-next db bind writes exactly this patch for you (see the CLI reference).

nextapp.yaml (BYO database)
spec:
  database:
    secretRef: { name: acme-db }          # key defaults to DATABASE_URL
    roSecretRef: { name: acme-db }        # key defaults to DATABASE_URL_RO

A NextApp only binds a Secret in its own namespace — it can never name or reach into another app's database. spec.database owns DATABASE_URL/DATABASE_URL_RO: a conflicting secrets.envMap entry for those names is rejected rather than silently overridden. Point the DSN at a connection pooler for scale-to-zero apps, and consume the injected DSNs with a typed client — see the Databases guide and the Data SDK.

Secrets live in Kubernetes Secrets / env only — never in spec.env, config files, images, or URLs. spec.env values are stored verbatim in the CR and visible to anyone who can read it.

Status fields

FieldNotes
urlThe resolved Knative Service URL.
conditionsReady, Degraded, Reconciling, DatabaseReady — see below.
currentTrafficMirror of the ksvc traffic split.

The operator reports honest readiness: Ready mirrors the underlying Knative Service's own Ready condition rather than reporting success as soon as the resource is applied — a CR whose service never comes up shows Ready: False with the real reason (e.g. the image-pull or crash detail). kn-next status renders these conditions verbatim and exits non-zero when Ready is False — see the CLI reference.

DatabaseReady reports binding health for a bring-your-own database. When spec.database binds a Secret, a successfully bound app shows DatabaseReady: True with reason Bound (message: "Bound existing Secret … as DATABASE_URL"). It is a binding-health signal, not a provisioning one — knext binds an existing Secret, it does not provision or manage the database. When spec.database is empty the condition is not set.

Two otherwise-silent failure modes get first-class diagnoses:

  • Ingress never programs. If the Knative Service reports its ingress as not-yet-configured for more than two minutes, the operator flips Ready to False with reason IngressNotProgrammed and emits a Warning event naming the usual fix — the config-network ConfigMap's ingress-class must be the full controller-qualified value (kourier.ingress.networking.knative.dev; see Installing knext). Without this, a misconfigured ingress class looks like an app that is "still starting" forever.
  • A pinned revision no longer exists. If spec.traffic pins a revision that has been deleted (e.g. garbage-collected), Knative keeps serving the last-good route with only an opaque internal condition. The operator instead performs its own existence check and — after a short stall window that rules out an in-progress deploy — reports Ready: False / Degraded with reason PinnedRevisionNotFound, plus a Warning event telling you to list the surviving revisions and re-pin (or clear spec.traffic). See Rollback & traffic split.

ISR revalidation & the Kafka queue

knext's ISR / data cache is Redis (cache.provider: redis). revalidateTag() and revalidatePath() write through the Redis-backed cache handler, and because the Redis keys are shared across every pod, an invalidation is fleet-wide — one pod's revalidateTag is seen by all of them. This is the default path and requires no queue.

The spec.revalidation block opts into running revalidation asynchronously over a queue, so regeneration does not block the user's request:

nextapp.yaml (revalidation)
spec:
  revalidation:
    queue: kafka
    kafkaBrokerUrl: kafka-cluster-kafka-bootstrap.kafka.svc:9092

Honest scope — provisionKafkaSource no longer does anything. This field used to opt into provisioning a KafkaSource, on the understanding that you had deployed your own <app>-revalidator consumer. That bring-your-own path is withdrawn: the sink contract — what the consumer must be called, which events it receives, how it authenticates — was never specified or tested, so the option could only aim events at a Service that might never exist.

No KafkaSource is provisioned for any value of the field. Your manifest still applies and your app still deploys normally; setting the field reports a RevalidationDeferred condition with reason ProvisionKafkaSourceInert plus a warning event, so an ignored setting is visible rather than silent. queue: kafka on its own behaves the same way, with reason ConsumerNotProvisioned.

Cache invalidation is unaffected — revalidateTag() is already fleet-wide through the shared Redis-backed cache. Kafka-driven revalidation returns when knext ships the consumer itself.

Security defaults

The operator is also the policy boundary:

  • Digest pinning is enforced. A CR whose image is :latest, tag-only, or untagged is rejected (Degraded / reason InvalidImage) and never deployed. The validator accepts a ref only if it contains @sha256:.
  • Mutating endpoints require auth. App-level cache-invalidation routes require a signed bearer token; nothing mutating is open by default.
  • Default-on NetworkPolicy. spec.security.networkPolicy reconciles an internal-only network policy for the app.
  • Cluster-local backends (design-stage). The planned gRPC backend layer is cluster-local with no public ingress — designed, not yet shipped.

Because only the operator mutates cluster state, your Git history of NextApp resources is the deployment history — auditable, reproducible, portable across clouds.

On this page