knext

Versioning & compatibility

What a knext version number promises, which releases get fixes, and how the CLI, the packages and the operator fit together.

knext gives you one number to pin, and a small set of rules about what that number means.

One number covers three packages

knext publishes three packages, and they are always released together at the same version:

  • @getknext/core — the knext CLI, the Next.js adapter and the runtime
  • @getknext/lib — the app-facing helpers (clients, health, logging)
  • @getknext/db — the data SDK and migration runner

@getknext/core depends on the other two, so they are versioned in lockstep. Pin one and you have pinned all three:

{
  "dependencies": {
    "@getknext/core": "^1.0.0"
  }
}

Releases before this rule took effect carry different numbers for the three packages. Those versions are internally consistent — a matching set always resolves — so nothing needs changing on your side.

What the number promises

The version follows semantic versioning over the documented public API:

  • Patch and minor releases never break a public import. New capabilities arrive in minors.
  • Anything that removes or changes the shape of a public import requires a major. That covers the knext CLI too: a removed flag, or a knext.config.ts key that stops being accepted, is a major change.
  • Imports under @getknext/core/internal/ carry no guarantee. They are framework wiring and can change in any release. If you need something that only exists there, open an issue — that is a signal the capability belongs on the public surface.

From 1.0 on, npm's default caret range (^1.0.0) picks up every later 1.x minor and patch, which the rules above make safe. (Before 1.0, a caret range on a 0.x version did not pick up the next minor, so 0.x upgrades were by hand.)

Each stable release is published to npm's latest tag. When a newer minor takes latest, an older line stays installable: npm install -D @getknext/core@1.0 resolves the newest 1.0.x, and the latest-1.0 dist-tag points at the same release.

Release candidates for a coming minor are published under their own dist-tag, never latest, so a plain npm install @getknext/core always gives you a stable release. The next dist-tag carries the 1.3 release candidates. A release candidate is a preview of a release that has not shipped yet: try it in a branch or a staging cluster, not in production, and expect it to change before the final release.

The three packages depend on each other with caret ranges, so an older @getknext/core can still resolve a newer @getknext/lib or @getknext/db once a later minor exists. If you stay on an older line, list all three packages in your own package.json at the same version.

Not every feature is covered by this policy yet — see Stability tiers for which features are Stable (covered above), Beta (may still change with notice), or Experimental (outside this policy entirely).

Before a capability is removed

Removal always takes two releases:

  1. It is marked deprecated in a minor release — visible in your editor through the types, named in the changelog, with a replacement to move to.
  2. It is removed no earlier than the next major.

So there is always a version you can run that has both the warning and the old behaviour, which is your migration window.

Which releases get fixes

The latest release line is the supported one. Fixes, including security fixes, land in the next release. There is no long-term-support promise for an older minor — knext has one maintainer, and a maintenance promise that went unmet would be worse than none.

In practice that means: upgrade forward to get a fix. Because minors never break the public surface, that is a version bump and a redeploy.

Security fixes are called out in the release notes for the release that carries them.

When a new major ships, the previous one gets a bounded window. From the day @getknext/core 2.0 is generally available, the final 1.x minor receives security fixes only, for six months, published under the latest-1 dist-tag (npm install @getknext/core@latest-1). After that it receives nothing. Before 2.0 is generally available, the paragraph above is the whole policy.

Supported runtimes on 1.x

The 1.x line runs on these runtimes:

  • Node.js >=22.18
  • Next.js >=16.0.0 <16.5.0

These are the floors and bound the published @getknext/core declares (engines and the next peer dependency), so your package manager warns you when a project is outside them. New apps scaffold with a recent Next.js release rather than the floor.

Which Next.js versions are covered

The next peer range is bounded: it admits only the Next.js minors knext has compatibility evidence for (>=16.0.0 <16.5.0), and the upper bound is widened one minor at a time, in a knext release, as evidence for the next minor lands.

Inside that range, the minors covered by the official Next.js compatibility suite are 16.3. The other admitted minors work, but the compatibility results do not describe them, and knext doctor prints a warning when your installed Next.js is not one of the covered minors.

A Next.js minor above the range is refused until a knext release admits it, and how you see that depends on your package manager: npm stops the install with a peer-dependency conflict (ERESOLVE), pnpm installs and prints an unmet-peer warning, and Bun installs without a message. To try a newer minor before knext admits it, npm install --legacy-peer-deps lets npm proceed; knext makes no compatibility claim for that combination.

The opt-in vinext build target, which compiles to a Bun single-executable, is Beta on every 1.x release. It follows the same release policy as the rest of the package, but it may still change with notice and is not part of the stability promise above; see Stability tiers.

What 2.0 changes

Version 2.0 is not yet released, so everything in this section describes what is planned, and the release notes of the day are the authority. A major is the one place the rules above allow breaking changes, and 2.0 is used to clear out what has been deprecated through 1.x:

  • The Node.js and Next.js floors rise. The new floors are stated in the 2.0 release notes; nothing here promises them earlier.
  • Deprecated imports and commands are removed. That includes the deprecated second name for the command; use knext. Removals follow the rule above: only something already deprecated, with a replacement named, is removed.
  • The operator does not get a new major. It stays on its own line, and the NextApp resource keeps its API version, so the ordering rule below still applies unchanged.

Staying on 1.x is supported for the window described above; the latest-1 dist-tag is the way to install it.

The cluster side versions separately

The operator that runs in your cluster, and the NextApp resource it reconciles, are not on the npm version line. The operator ships as a container image with its own install bundle, and the resource is on the Kubernetes API ladder:

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

A major version of @getknext/core does not change that value, and a change to that value does not force a major of the packages.

Within apps.kn-next.dev/v1alpha1 the schema is additive-only: fields are added, never removed or narrowed. A NextApp you write today keeps applying against later operators. What can change is what a field does — a setting for a capability that has been superseded may become inert. That is called out in the release notes, and the operator may also record a warning event on the resource:

kubectl describe nextapp my-app   # check the Events section after an upgrade

It never turns into a rejected apply.

The operator's major moves only when the operator breaks

The operator has its own version line, and a new major of @getknext/core does not give it one. Its major changes only when the operator itself makes a change that cannot be additive — a new API version of the resource that needs a conversion path, say. A @getknext/core 2.0 therefore pairs with the same operator line as 1.x; what you check is the resource's API version and the ordering rule below, not that two version numbers match.

Keeping the two in step

You do not need matching versions on the two sides. You need one ordering rule:

Upgrade the operator and its CRD first, then the CLI.

An older CLI against a newer cluster is always fine. The reverse is not, and knext deploy stops before it changes anything when it detects it. See Upgrading for what that looks like and how to fix it.

Operator versions

Operator releases are tagged operator-vX.Y.Z and follow semantic versioning on their own line. On the 1.x line the minor tracks the package set (an operator 1.3.x pairs with @getknext/core 1.3.x); the patch is independent, so an operator-only fix does not force a package release. A new major of the packages does not give the operator a new major. What decides whether a CLI and an operator pair is the NextApp API version they share (apps.kn-next.dev/v1alpha1), and, within it, the operator release being at least as new as the CLI. The pairing rule is:

Operator vs. CLIResult
Operator serves the CLI's apps.kn-next.dev/v1alpha1, release equal or newerSupported. Patch levels do not matter.
Operator serves the CLI's apps.kn-next.dev/v1alpha1, release older than the CLINot supported — upgrade the operator first.
Operator does not serve the API version the CLI emitsNot supported.

Pre-release suffixes (-rc.N) are ignored when pairing.

knext doctor checks the first half of that rule against your cluster: its NextApp CRD row reads the API versions the installed CRD serves and compares them with the apiVersion your CLI emits. It passes when that version is served (a CRD that serves several versions is fine as long as one of them is yours) and fails when the CRD does not serve it. The message tells you which side to move: upgrade the operator first when the CRD only serves older versions than your CLI emits, or upgrade the CLI to match the operator when it only serves newer ones (a version marked deprecated still counts as served). If the CRD cannot be read, the row fails rather than guessing. The Operator version row, described below, is informational and does not decide the pairing.

Pin, upgrade and roll back the operator

Every operator release is published as a GitHub Release named after its tag, carrying two copies of the same digest-pinned bundle: install-vX.Y.Z.yaml (the version is in the file name, so a saved copy says what it is) and install.yaml. A release never changes after it is published. operator-latest keeps working and always carries the newest stable release's install.yaml; use it for a quick trial, not for a cluster you have to reproduce.

See which operator you run

shell
knext doctor

The Operator version row prints the installed release, where it was read from, and the image digest, then states whether it pairs with your CLI:

PASS  Operator version  v1.3.0 (from label), image sha256:9f1c0a7d2b44…; compatible with CLI 1.3.0

It warns, without failing the preflight, when the operator is older than the CLI on the same major version, when the operator is an unreleased build, or when it predates operator versions and reports none. A different major version is not a warning: the operator keeps its own version line when @getknext/core moves to 2.0, and the row says the pairing is decided by the CRD API version. The version is stamped on the manager Deployment, so you can also read it directly:

shell
kubectl get deploy kn-next-operator-controller-manager -n kn-next-operator-system \
  -o jsonpath='{.metadata.labels.app\.kubernetes\.io/version}{"\n"}'

Pin a version

Apply the versioned bundle of the release you tested, instead of a moving address:

shell
VERSION=1.3.0   # an operator-vX.Y.Z release from the project's GitHub Releases page
kubectl apply --server-side -f \
  "https://github.com/getknext-dev/knext/releases/download/operator-v${VERSION}/install-v${VERSION}.yaml"

For GitOps, commit that exact file (or reference that exact URL) so the operator version moves only when you change it. The image in the bundle is referenced by digest and is signed; check the signature before you trust a bundle, as described under Install.

Upgrade to a specific version

Apply the new version's bundle with the same command, then wait for the rollout and check the result:

shell
kubectl apply --server-side -f \
  "https://github.com/getknext-dev/knext/releases/download/operator-v${NEW_VERSION}/install-v${NEW_VERSION}.yaml"
kubectl rollout status deploy/kn-next-operator-controller-manager \
  -n kn-next-operator-system --timeout=180s
knext doctor

Applying the bundle also applies its NextApp CRD. Keep the order from above: operator first, then the CLI.

Roll back to a previous version

Re-apply the previous version's bundle. It reverts the image and the CRD together:

shell
kubectl apply --server-side -f \
  "https://github.com/getknext-dev/knext/releases/download/operator-v${PREVIOUS_VERSION}/install-v${PREVIOUS_VERSION}.yaml"
kubectl rollout status deploy/kn-next-operator-controller-manager \
  -n kn-next-operator-system --timeout=180s

Your running apps keep serving while the operator is rolled back. Before you roll back across a release that added a field to the NextApp resource, check that release's notes: the older CRD does not know the field, so a value set only through it is dropped the next time that resource is written. If the release added no field, nothing is lost.

An operator installed before versions were published (or from the rolling operator-edge channel) reports no release version. Move it to a versioned release once, with the pin command above, and every later upgrade and rollback has a concrete target.

On this page