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, reachableThe 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.
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 / ReconcilingSpec fields (selected)
| Field | Notes |
|---|---|
image | Required. Container image — must be digest-pinned (@sha256:). |
scaling | minScale, maxScale, containerConcurrency. |
resources | cpuRequest, 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. |
storage | provider, bucket, region, endpoint. |
cache | provider, 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. |
revalidation | queue, kafkaBrokerUrl, provisionKafkaSource — async ISR revalidation (see below). |
env | Plain, non-secret env vars (name → value). Reserved names are rejected at admission (see below). |
secrets | envFrom, envMap (Kubernetes Secret refs) — see Binding secrets. |
database | Bind a bring-your-own Postgres by referencing an existing Secret (secretRef / optional roSecretRef). knext does not provision databases. See Databases. |
runtime | bun 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. |
security | networkPolicy — pod-level L3/L4 isolation, default-on. |
traffic | revisionName, canaryPercent (rollback / canary). |
buildId | Next.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, andK_CONFIGURATIONare rejected when you apply the resource — they are managed by the platform, and overriding them would break request routing. - If an
envname collides with a platform-managed variable or asecrets.envMapmapping, theenventry 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:
spec:
secrets:
envMap:
DATABASE_URL:
secretName: acme-db-app
secretKey: pooler-urlThis 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).
spec:
database:
secretRef: { name: acme-db } # key defaults to DATABASE_URL
roSecretRef: { name: acme-db } # key defaults to DATABASE_URL_ROA 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
| Field | Notes |
|---|---|
url | The resolved Knative Service URL. |
conditions | Ready, Degraded, Reconciling, DatabaseReady — see below. |
currentTraffic | Mirror 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
ReadytoFalsewith reasonIngressNotProgrammedand emits a Warning event naming the usual fix — theconfig-networkConfigMap'singress-classmust 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.trafficpins 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 — reportsReady: False/Degradedwith reasonPinnedRevisionNotFound, plus a Warning event telling you to list the surviving revisions and re-pin (or clearspec.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:
spec:
revalidation:
queue: kafka
kafkaBrokerUrl: kafka-cluster-kafka-bootstrap.kafka.svc:9092Honest 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
imageis:latest, tag-only, or untagged is rejected (Degraded/ reasonInvalidImage) 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.networkPolicyreconciles an internal-only network policy for the app. - Cluster-local backends (design-stage). The planned gRPC backend layer is
cluster-localwith 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.