diff --git a/.github/workflows/docs-gen-and-push.yaml b/.github/workflows/docs-gen-and-push.yaml
new file mode 100644
index 000000000..fbd4f223f
--- /dev/null
+++ b/.github/workflows/docs-gen-and-push.yaml
@@ -0,0 +1,89 @@
+name: Docs
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - v2-next
+ paths:
+ - "docs/**"
+ - "cli/**"
+ - "cmd/**"
+ - "backend/**"
+ - "engine/**"
+ - "pkg/**"
+ - "deploy/charts/**"
+ - "sdk/apis/**"
+ - "go.mod"
+ - "go.sum"
+ - "sdk/go.mod"
+ - "sdk/go.sum"
+ - "Makefile"
+ - ".github/workflows/docs-gen-and-push.yaml"
+ push:
+ branches:
+ - v2-next
+ - docs-v2
+ paths:
+ - "docs/**"
+ - "cli/**"
+ - "cmd/**"
+ - "backend/**"
+ - "engine/**"
+ - "pkg/**"
+ - "deploy/charts/**"
+ - "sdk/apis/**"
+ - "go.mod"
+ - "go.sum"
+ - "sdk/go.mod"
+ - "sdk/go.sum"
+ - "Makefile"
+ - ".github/workflows/docs-gen-and-push.yaml"
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: "3.12"
+ cache: pip
+ cache-dependency-path: docs/requirements.txt
+ - run: make docs-venv build-docs
+
+ publish:
+ if: >-
+ github.repository == 'kbind-dev/kbind' &&
+ github.event_name != 'pull_request' &&
+ (github.ref == 'refs/heads/v2-next' || github.ref == 'refs/heads/docs-v2')
+ needs: build
+ permissions:
+ contents: write
+ concurrency:
+ group: Docs
+ cancel-in-progress: false
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: "3.12"
+ cache: pip
+ cache-dependency-path: docs/requirements.txt
+ - name: Publish preview without changing latest
+ env:
+ PUSH: "1"
+ run: |
+ git config user.name "kbind-ci-bot"
+ git config user.email "no-reply@kbind.dev"
+ make docs-venv deploy-docs
diff --git a/Makefile b/Makefile
index 768f8efc0..8edf2b120 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,8 @@ SETUP_ENVTEST ?= go run sigs.k8s.io/controller-runtime/tools/setup-envtest@rele
CHART ?= deploy/charts/konnector-v2
BACKEND_CHART ?= deploy/charts/backend-v2
IMAGE ?= ghcr.io/kbind-dev/konnector:dev
+DOCS_VENV ?= $(CURDIR)/docs/.venv
+CRD_REF_DOCS ?= go run github.com/elastic/crd-ref-docs@v0.3.0
.PHONY: all
all: codegen build
@@ -177,3 +179,36 @@ helm-push:
echo "==> pushing $$chart-$(CHART_VERSION).tgz to oci://$(HELM_REPO)"; \
$(HELM) push bin/charts/$$chart-$(CHART_VERSION).tgz oci://$(HELM_REPO) || exit 1; \
done
+
+.PHONY: docs-venv
+docs-venv:
+ python3 -m venv $(DOCS_VENV)
+ $(DOCS_VENV)/bin/python -m pip install --upgrade pip
+ $(DOCS_VENV)/bin/python -m pip install -r docs/requirements.txt
+
+.PHONY: generate-cli-docs
+generate-cli-docs:
+ mkdir -p docs/content/reference/cli
+ go run ./docs/generators/cli-doc > docs/content/reference/cli/index.md
+
+.PHONY: generate-api-docs
+generate-api-docs:
+ mkdir -p docs/content/reference/crd
+ $(CRD_REF_DOCS) --source-path=./sdk/apis \
+ --config=docs/generators/crd-ref/config.yaml --renderer=markdown \
+ --output-path=docs/content/reference/crd/index.md
+
+.PHONY: generate-docs
+generate-docs: generate-cli-docs generate-api-docs
+
+.PHONY: build-docs
+build-docs: generate-docs
+ cd docs && $(DOCS_VENV)/bin/mkdocs build --strict
+
+.PHONY: serve-docs
+serve-docs: generate-docs
+ cd docs && $(DOCS_VENV)/bin/mkdocs serve
+
+.PHONY: deploy-docs
+deploy-docs: build-docs
+ PATH="$(DOCS_VENV)/bin:$$PATH" bash docs/scripts/deploy-docs.sh
diff --git a/README.md b/README.md
index b5f9cafef..90fc15301 100644
--- a/README.md
+++ b/README.md
@@ -1,286 +1,77 @@
-# kbind
-
-kbind binds APIs exported by a provider cluster into a consumer cluster. The
-**slim core** is a consumer-side sync engine (the konnector) consuming a
-one-apply bundle; the optional **extended layer** (backend gateway + issuer +
-reaper, CLI, UI) produces that bundle for humans. See
-[docs/proposals/v2-slim-core.md](docs/proposals/v2-slim-core.md) and
-[docs/proposals/v2-extended.md](docs/proposals/v2-extended.md) for the designs.
-
-## Layout
+
-```
-. # module: github.com/kbind/kbind (konnector + service layer)
-├── go.work # ties the two modules for local dev
-├── cmd/konnector/ # main: local mgr + mcmanager + reconcilers
-├── cmd/backend/ # main: kbind-backend (gateway/issuer/reaper module flags)
-├── cmd/bind/ # main: the bind CLI (login, catalog, bind an export)
-├── engine/
-│ ├── provider/ # mcr provider: Connection -> engaged cluster
-│ ├── connection/ # resolve secret, pin identity, discover exports
-│ ├── binding/ # validate + pull CRDs (schema.source: CRD)
-│ ├── sync/ # per-GVR spec-up / status-down + conflicts
-│ └── remote/ # kubeconfig + cluster identity helpers
-├── backend/
-│ ├── auth/ # Authenticator iface, OIDC (+ --oidc-mock), sessions
-│ ├── issuer/ # Issuer iface + kube impl, Grant/Export controllers, bundle
-│ ├── gateway/ # HTTP API: catalog, bind, one-time bundle pickup, apply
-│ └── reaper/ # Lease-keyed GC of stale Grants
-├── cli/ # bind CLI packages (client, login dance, commands)
-├── web/ # embedded gateway UI (dependency-free SPA)
-├── pkg/
-│ ├── kubeapply/ # SSA apply helper for bundles/manifests
-│ └── konnectorinstall/ # embedded konnector install (CRDs, RBAC, Deployment)
-├── test/e2e/ # two-envtest end-to-end suite (core + backend loop)
-├── deploy/charts/konnector/ # consumer-side Helm chart (CRDs, RBAC, Deployment)
-├── deploy/charts/backend/ # provider-side Helm chart (service layer)
-├── sdk/ # module: github.com/kbind/kbind/sdk (type-only API)
-│ ├── apis/core/v1alpha1/ # Connection, ClusterBinding, Binding (core.kbind.io)
-│ ├── apis/catalog/v1alpha1/ # Export, Collection (catalog.kbind.io)
-│ ├── apis/iam/v1alpha1/ # Grant (iam.kbind.io)
-│ └── config/crd/ # generated CRD manifests
-└── hack/demo.sh # two-kind-cluster end-to-end demo
-```
+[](https://github.com/kbind-dev/kbind/blob/main/LICENSE)
+[](https://github.com/kbind-dev/kbind/releases/latest)
-## What works today (POC milestone: E2E single-API sync)
+# kbind
-- `Connection` resolves a kubeconfig Secret, pins provider/consumer cluster
- identity, and discovers label-gated exported CRDs into `status.exportedAPIs`.
-- `ClusterBinding` / `Binding` validate the connection, pull the listed CRDs
- (`schema.source: CRD`, single served version, no conversion webhook) onto the
- consumer, and report `Ready` + `boundAPIs`.
-- A dynamic per-GVR syncer copies instance **spec up** (server-side apply with
- ownership markers + a finalizer) and **status down**. `conflictPolicy: Fail`
- refuses a foreign provider target (Event + conflict annotation, counted on the
- binding's `conflictCount` + `Conflicts` condition); `conflictPolicy: Adopt`
- takes over an *un-owned* provider object (never one owned by another binding).
-- The Connection **re-discovers** exported APIs periodically, so a CRD labeled
- `exported` after connect is picked up and its binding goes Ready.
-- Schema knobs are honored: `pullPolicy: Bound`/`All`/`None`, `updatePolicy:
- Always`/`Once`, and `autoBind` (a managed ClusterBinding mirroring exported
- APIs). `deletion-policy: Orphan` keeps a provider copy on delete/unbind.
- Provider RBAC denials surface as a `PermissionDenied` condition / Event.
-- `schema.source: OpenAPI` (and `Auto`) synthesizes the consumer CRD from the
- provider's discovery + `/openapi/v3` — the CRD-less (kcp-like) path — and the
- Connection installs it. Known fidelity limits (CEL, defaulting, `$ref`,
- multi-version) are accepted; the provider stays the enforcing side.
-- The konnector maintains a `coordination.k8s.io/Lease` per Connection on the
- provider (heartbeat) — the hook a service-layer reaper keys off.
-- **kcp-aware cluster identity**: the provider's stable identity is the kcp
- `LogicalCluster` ("cluster") UID when present (a kcp workspace serves
- `core.kcp.io`, and has no `kube-system`), falling back to the `kube-system`
- namespace UID on plain Kubernetes. A provider RBAC denial on the identity read
- surfaces as `PermissionDenied`.
-- `relatedResources` sync selected Secrets/ConfigMaps in the declared direction,
- scoped like the binding, GC'd when they stop matching or on unbind.
-- The provider side is the **multicluster-runtime engaged cluster** for each
- Connection: writes go through its client, fresh reads through its API reader,
- and status/drift events arrive via a **watch on its cache** (event-driven, not
- polled — a low-frequency resync is only a backstop).
-- **Stop-on-disengage**: a Connection that loses readiness (revoked credential,
- unreachable provider, withdrawn RBAC) is disengaged, and its per-GVR syncers
- are torn down rather than left running against a dead cluster. When it becomes
- Ready again the provider re-engages as a fresh cluster and the syncers are
- rebuilt against it (a stale syncer would otherwise hold a dead client forever).
-- **Mapper extension point** (`engine/mapper`): the syncer routes every
- provider-side operation through a `Mapper` that translates the consumer object
- key to its provider key. Core ships only `Identity` (ns/name unchanged); an
- out-of-tree build supplies its own via `sync.WithMapper(...)` to restore v1's
- "Prefixed" key isolation without forking the engine. The interface maps keys
- only — it cannot change scope (cluster-scoped stays cluster-scoped), and it is
- deliberately kept out of the CRD API so the core API never promises renaming.
-- **Order-independent apply**: a `Connection` created before its Secret resolves
- when the Secret arrives (the konnector watches referenced Secrets); a binding
- created before its Connection resolves when the Connection goes Ready.
-- **Complete unbind**: `Connection` and bindings carry a cleanup finalizer.
- Deleting a `ClusterBinding` deletes the provider copies of synced instances,
- releases instance finalizers, and removes the pulled CRD (cascading the
- instances). A `Connection` blocks (`DrainingBindings`) until its bindings are
- gone, and keeps its Secret alive (via a finalizer) so teardown can still reach
- the provider — so `kubectl delete -f bundle.yaml` is order-don't-care.
+You are invited to [contribute](#contributing)!
-Known POC simplifications (tracked against the proposal): OpenAPI synthesis is
-best-effort (fidelity limits above); the `Mapper` seam exists but only `Identity`
-ships and `relatedResources` are not yet routed through it.
+## What is it?
-## The extended layer (backend, CLI, UI)
+kbind (formerly known as kube-bind) provides better support for service providers and consumers that reside in distinct Kubernetes clusters.
-The service layer is optional — GitOps against the core objects works without
-any of it. It answers what the core deliberately doesn't: who are you (OIDC),
-what may you have (catalog), here are your credentials (issuer), here is your
-bundle (gateway), you stopped coming (reaper).
+- A service provider defines its API in terms of CRDs, exports it for use from other clusters, and controls access through Kubernetes RBAC.
+- Service consumers identify the services they want to consume.
+- The service CRDs get installed in the service consumer clusters, with objects of the defined kinds written and read by the service consumers.
+- The service provider indirectly reads and writes those objects as the interface to the service that it provides.
+- The service provider does not inject controllers/operators into the service consumer's cluster.
+- A single vendor-neutral, OpenSource agent per consumer cluster, the **konnector**, connects it with the requested services.
-- **One binary, module flags**: `kbind-backend` runs on/against the **provider**
- cluster with `--enable-gateway` (HTTP API + UI), `--enable-issuer` (Grant
- provisioning + catalog validation), `--enable-reaper` (off by default) and
- `--enable-apply` (browser-apply, off by default).
-- **Catalog**: curate offerings as `Export`/`Collection` (`catalog.kbind.io`)
- on the provider; an Export listing a non-exported API is hidden until the
- `core.kbind.io/exported` label appears. An Export's
- `defaults.relatedResources` (the core's secrets/configmaps-alongside-the-API
- concept) flow into the Grant, the issued RBAC and the bundle's
- ClusterBinding — and are shown on the catalog card (`⇩ secrets`-style chips)
- so consumers see what will flow before binding.
-- **Issuance**: every bind records a `Grant` (`iam.kbind.io`) — identity,
- export, resolved APIs — and the issuer provisions a boundary namespace, a
- ServiceAccount with RBAC enumerating exactly the granted APIs
- (`--issuer-scope Cluster|Namespace`), and a long-lived SA token. Deleting the
- Grant revokes. The issued kubeconfig pins its context namespace to the
- boundary, which is where the konnector's heartbeat Lease lands — the reaper's
- signal.
-- **Clusters view**: `GET /api/clusters` (UI tab "Clusters", CLI
- `kubectl bind clusters`) aggregates the konnector heartbeat Leases into a
- consumer-cluster inventory — which clusters are live, what each has bound
- (grant/export/identity, heartbeat age) and how many objects it is syncing
- per API (counted via the sync engine's ownership markers). Grants whose
- bundle was never applied show up as "bound, never connected".
-- **Catalog instances**: `GET /api/catalog//instances` (an expandable
- "Synced instances" panel on each catalog item; CLI `kubectl bind instances
- `) lists the provider-side objects consumers synced under an
- offering — namespace/name, owning cluster, identity (via the heartbeat
- Lease → Grant link) and age.
-- **Connect a cluster**: `GET /api/konnector` serves the konnector install
- (CRDs, RBAC, Deployment) as one apply-able YAML — no auth, no credentials
- inside, so `curl -fsS /api/konnector | kubectl apply -f -` onboards
- a cluster. Surfaced as a "Connect a cluster" button in the Clusters view
- (with a gateway-side install when `--enable-apply` is on) and as
- `kubectl bind connect` in the CLI.
-- **Bundle delivery**: `POST /api/bind` returns a **one-time pickup URL**
- (5-minute TTL); the pickup itself is single-use, the credentials inside stay
- valid until revoked (GitOps-safe). The gateway is stateless — sessions are
- encrypted cookies/bearer tokens, single-use is enforced through annotations
- on the Grant — so replicas just need shared `--cookie-*-key`s.
-- **Auth**: OIDC with kube-apiserver-style flags (`--oidc-issuer-url`,
- `--oidc-client-id`, `--oidc-client-secret`, `--oidc-username-claim`,
- `--oidc-groups-claim`, `--oidc-scopes`, `--oidc-ca-file`,
- `--oidc-redirect-url`), or `--oidc-mock` for a dev issuer that auto-approves.
- `--kubernetes-auth` additionally accepts provider-cluster bearer tokens
- (verified via TokenReview) — for in-platform callers that already hold a
- cluster identity and shouldn't need a second SSO round trip.
+## Try it out
-The CLI installs via krew under the plugin name **`bind`** (kept from v1):
-`kubectl krew install bind`, then `kubectl bind …` — or grab the `kbind`
-binary from a release / `make bind`.
+Follow the [Quickstart](docs/content/setup/quickstart.md) to try v2 with two local Kubernetes clusters. It runs from source without depending on a published v2 release.
-```sh
-# dev loop against the current kubeconfig context as the provider:
-go run ./cmd/backend --oidc-mock --external-url http://localhost:8080
+With the [CLI installed](docs/content/setup/kubectl-plugin.md), a [provider offering a `widgets` API](docs/content/developers/backend/index.md), and the [v2 konnector installed](docs/content/setup/helm.md) in your consumer cluster:
-kubectl bind login http://localhost:8080 # browser dance, caches the session
-kubectl bind connect # install the konnector into your cluster
-kubectl bind catalog # list Exports/Collections
-kubectl bind export mangodb # bundle → konnector install → apply
-kubectl bind export mangodb -o yaml > b.yaml # GitOps mode: print, don't apply
-kubectl bind clusters # which consumer clusters sync what
-# ...or no CLI at all:
-curl -fsS | kubectl apply -f -
+```shell
+kubectl bind login https://bind.example.com
+kubectl bind export widgets --kubeconfig=./consumer.kubeconfig --install-konnector=false
```
-The UI (embedded in the gateway, `/`) browses the catalog, binds, and hands
-out the bundle ticket; browser-apply appears only when `--enable-apply` is on.
+Replace the provider URL and consumer kubeconfig with your own. The konnector makes the Widget API available in the consumer cluster, without a Widget-specific controller running there. See the [CLI guide](docs/content/setup/kubectl-plugin.md) for details.
-## Build
+## For more information
-```sh
-make build # builds both modules (workspace mode via go.work)
-make konnector # builds the konnector binary into ./bin
-make backend # builds the kbind-backend binary into ./bin
-make bind # builds the bind CLI into ./bin
-```
+For more information go to https://kbind.dev or watch the [ContainerDays talk](https://www.youtube.com/watch?v=dg0g15Qv5Fo&t=1s) or the [KubeCon talk](https://www.youtube.com/watch?v=Uv0ivz5xej4).
-## Deploy
+kbind is following this manifesto from the linked talk:
-The konnector runs in (or against) the **consumer** cluster — it is the only
-running component of the core (no backend, no provider-side controllers).
+Let's design a post-operator / post-cluster technology, that allows a service provider persona as a first-class citizen and can securely provide centrally operated kube-native services.
-```sh
-make image IMAGE=ghcr.io/kbind-dev/konnector:dev # build the image
-helm install konnector deploy/charts/konnector \
- -n kbind --create-namespace \
- --set image.repository=ghcr.io/kbind-dev/konnector --set image.tag=dev
-```
+## Contributing
-The chart ([deploy/charts/konnector](deploy/charts/konnector)) ships the core
-CRDs (`installCRDs`, default on), a `ServiceAccount`, the consumer RBAC
-(`ClusterRole`/`ClusterRoleBinding` + a namespaced leader-election `Role`), and
-the `Deployment` with liveness/readiness probes. Notable values:
+We ❤️ our contributors! If you're interested in helping us out, please check out
+[Contributing to kbind](docs/content/contributing/index.md) and [kbind Project Governance](https://github.com/kbind-dev/kbind/blob/main/GOVERNANCE.md).
-- `replicaCount` / `leaderElect` — HA. Leader election gates all consumer-side
- controllers, so standby replicas engage no providers until they win the lease.
- It is forced on automatically when `replicaCount > 1`.
-- `rbac.boundResourceGroups` (default `["*"]`) — the API groups the konnector may
- sync. The bound APIs are open-ended, so this defaults to all groups; narrow it
- to the specific groups your providers export to shrink the blast radius.
+The [developer guide](docs/content/developers/index.md) covers the architecture, development environments, builds, tests, and code generation.
-Provider credentials are governed by the kubeconfig in each `Connection`'s
-Secret, **not** the konnector's ServiceAccount — the RBAC above is consumer-side
-only.
+## Getting in touch
-The **backend** deploys on the provider with its own chart
-([deploy/charts/backend](deploy/charts/backend)):
+There are several ways to communicate with us:
-```sh
-make image-backend BACKEND_IMAGE=ghcr.io/kbind-dev/backend:dev
-helm install backend deploy/charts/backend \
- -n kbind-system --create-namespace \
- --set externalURL=https://kbind.example.com \
- --set oidc.issuerURL=https://sso.example.com \
- --set oidc.clientID=kbind --set oidc.existingSecret=kbind-oidc \
- --set cookieKeys.existingSecret=kbind-cookie-keys
-```
+- The [`#kbind-dev` channel](https://kubernetes.slack.com/archives/C046PRXNJ4W) in the [Kubernetes Slack workspace](https://slack.k8s.io).
+- Our mailing list [kube-bind-dev](https://groups.google.com/g/kube-bind-dev) for development discussions.
+- Our bi-weekly community meetings, every second Thursday at 11am EST (5pm CET).
+ Join the mailing list for an invite and see our [community meeting notes](https://docs.google.com/document/d/1qztpKOmdZu5iWq_4N9n3AZpcAPuPhBiGNbje5GPg0iM) for upcoming and past agendas.
-Module flags map to `modules.{gateway,issuer,reaper,apply}` values; the chart
-ships the catalog/iam CRDs, a Service for the gateway, and RBAC that includes
-`escalate`/`bind` on Roles so the issuer may mint tenant Roles enumerating APIs
-the backend itself does not hold. For a dev install, `--set oidc.mock=true` is
-the only required auth setting.
+See the [community page](docs/content/community/index.md) for more details.
-## Codegen (after editing types)
+## Technical Overview
-```sh
-make codegen # regenerates deepcopy + CRDs under sdk/
-make helm-sync-crds # codegen + refresh the chart's bundled CRDs
-```
-
-## Tests
-
-The end-to-end test ([test/e2e](test/e2e)) runs two in-process **envtest** API
-servers (provider + consumer) with the real engine reconcilers: Connection Ready
-+ discovery → ClusterBinding Ready + CRD pull → spec up → status down → spec
-update → conflict (foreign object not overwritten) → deletion.
-`backend_test.go` closes the extended-layer loop on the same harness: catalog →
-gateway bind → issuer-provisioned credentials → one-time pickup → one apply →
-sync through the issued RBAC-fenced ServiceAccount → heartbeat in the boundary
-namespace → reaper → revocation.
-
-```sh
-make test # unit tests (no external setup)
-make test-e2e # downloads envtest assets and runs the e2e suite
-```
-
-## Run the demo (two kind clusters)
+
-```sh
-make demo # creates two kind clusters and wires the bundle
-# then follow the printed instructions to run the konnector and sync a Widget
-```
+The konnector connects directly to the provider's Kubernetes API. The backend
+adds a catalog, authentication, and credential issuance, but is not required for
+synchronization. See the [architecture guide](docs/content/developers/architecture.md) for the implementation details.
-## Tilt dev loop (two kind clusters)
+## Usage
-Tilt drives one kube-context per Tiltfile, so the two-cluster loop is split:
-the **provider** cluster (backend image + chart, port-forwards, rebuild on
-change) is managed natively, and the **consumer** cluster (konnector image +
-chart) is driven through `local_resource` steps against the second context —
-one `tilt up`, both clusters in one UI.
+To get familiar with setting up the environment, please check out the
+[setup guide](docs/content/setup/index.md) and [usage guide](docs/content/usage/index.md). For an existing installation, start with [Moving from 0.x](docs/content/usage/migration.md).
-```sh
-make tilt # kind clusters + backend (mock OIDC) + seeded catalog + konnector
-make tilt-down # tear both clusters down
-```
+### Limitations
-Then: open http://localhost:8080 (UI, login auto-approves via the mock
-issuer — its fixed in-pod port 5556 is forwarded so the browser can reach it),
-or `kubectl bind login http://localhost:8080` and
-`kubectl bind export widgets --kubeconfig `. Issued kubeconfigs
-point at `kbind-provider-control-plane:6443`, reachable from consumer pods on
-the shared kind docker network.
+Resource scope, namespace, and name are preserved between
+clusters. Namespace isolation must be arranged by the deployment.
+See [API concepts](docs/content/usage/api-concepts.md) for schema limitations and [Resource Synchronization](docs/content/usage/synchronization.md) for isolation and cleanup behavior.
diff --git a/deploy/charts/backend-v2/files/crds/catalog.kbind.io_collections.yaml b/deploy/charts/backend-v2/files/crds/catalog.kbind.io_collections.yaml
index 16752498c..9bc289b91 100644
--- a/deploy/charts/backend-v2/files/crds/catalog.kbind.io_collections.yaml
+++ b/deploy/charts/backend-v2/files/crds/catalog.kbind.io_collections.yaml
@@ -27,7 +27,7 @@ spec:
schema:
openAPIV3Schema:
description: |-
- Collection groups Exports for UI/CLI browsing. Pure presentation; nothing
+ Collection groups Exports for UI/CLI browsing. Pure presentation, nothing
binds to a Collection.
properties:
apiVersion:
diff --git a/deploy/charts/backend-v2/files/crds/catalog.kbind.io_exports.yaml b/deploy/charts/backend-v2/files/crds/catalog.kbind.io_exports.yaml
index 6717d76b2..ae36c4f1d 100644
--- a/deploy/charts/backend-v2/files/crds/catalog.kbind.io_exports.yaml
+++ b/deploy/charts/backend-v2/files/crds/catalog.kbind.io_exports.yaml
@@ -33,9 +33,9 @@ spec:
Export is one curated offering in the provider's catalog: human-facing
metadata plus defaults on top of one or more exported APIs. The catalog is
derived-from-core-truth: an Export listing an API that is not actually
- exported (label/boundary) gets a condition and is hidden by the gateway —
+ exported (label/boundary) gets a condition and is hidden by the gateway,
the export label remains the source of truth, the catalog is presentation
- and defaults. Lives on the provider; the konnector never sees it.
+ and defaults. Lives on the provider, the konnector never sees it.
properties:
apiVersion:
description: |-
@@ -65,7 +65,7 @@ spec:
description: |-
APIRef identifies an exported API by its CRD name on the provider,
i.e. "." (for example "mangodbs.mangodb.io"). Under the
- OpenAPI schema source there is no CRD object behind the name; it is still
+ OpenAPI schema source there is no CRD object behind the name, it is still
just resource + group.
properties:
name:
diff --git a/deploy/charts/backend-v2/files/crds/iam.kbind.io_grants.yaml b/deploy/charts/backend-v2/files/crds/iam.kbind.io_grants.yaml
index 12651a0e1..10e0a4729 100644
--- a/deploy/charts/backend-v2/files/crds/iam.kbind.io_grants.yaml
+++ b/deploy/charts/backend-v2/files/crds/iam.kbind.io_grants.yaml
@@ -38,7 +38,7 @@ spec:
description: |-
Grant records that an identity was issued credentials for an export. The
gateway creates it with the export's API list and defaults resolved in
- (issuance is a stable record even if the catalog entry changes later); the
+ (issuance is a stable record even if the catalog entry changes later), the
issuer controller provisions the tenancy boundary, ServiceAccount, RBAC and
token from the spec and reports the artifacts in status. Deleting the Grant
revokes: the issuer's cleanup finalizer unwinds everything it provisioned.
@@ -72,7 +72,7 @@ spec:
description: |-
APIRef identifies an exported API by its CRD name on the provider,
i.e. "." (for example "mangodbs.mangodb.io"). Under the
- OpenAPI schema source there is no CRD object behind the name; it is still
+ OpenAPI schema source there is no CRD object behind the name, it is still
just resource + group.
properties:
name:
diff --git a/deploy/charts/backend-v2/templates/NOTES.txt b/deploy/charts/backend-v2/templates/NOTES.txt
index 8d58392d5..f259fb0ac 100644
--- a/deploy/charts/backend-v2/templates/NOTES.txt
+++ b/deploy/charts/backend-v2/templates/NOTES.txt
@@ -8,14 +8,14 @@ Reach the gateway (port-forward for a quick look):
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "backend.fullname" . }} 8080:{{ .Values.service.port }}
open http://localhost:8080 # the catalog UI
- kbind login http://localhost:8080 # CLI login
+ kubectl bind login http://localhost:8080 # CLI login
{{- if not .Values.externalURL }}
-NOTE: externalURL is not set — OIDC callbacks and pickup URLs default to
+NOTE: externalURL is not set, OIDC callbacks and pickup URLs default to
localhost. Set it (and expose the Service) for real consumers.
{{- end }}
{{- if not .Values.cookieKeys.existingSecret }}
-NOTE: cookieKeys.existingSecret is not set — sessions are ephemeral and will
+NOTE: cookieKeys.existingSecret is not set, sessions are ephemeral and will
not survive restarts or work across replicas.
{{- end }}
{{- end }}
diff --git a/deploy/charts/konnector-v2/files/crds/core.kbind.io_bindings.yaml b/deploy/charts/konnector-v2/files/crds/core.kbind.io_bindings.yaml
index 54687fe19..d935588ab 100644
--- a/deploy/charts/konnector-v2/files/crds/core.kbind.io_bindings.yaml
+++ b/deploy/charts/konnector-v2/files/crds/core.kbind.io_bindings.yaml
@@ -65,7 +65,7 @@ spec:
description: |-
APIRef identifies an exported API by its CRD name on the provider,
i.e. "." (for example "mangodbs.mangodb.io"). Under the
- OpenAPI schema source there is no CRD object behind the name; it is still
+ OpenAPI schema source there is no CRD object behind the name, it is still
just resource + group.
properties:
name:
@@ -81,7 +81,7 @@ spec:
default: Fail
description: |-
conflictPolicy controls what happens when a target object already exists.
- Fail (default) leaves a foreign object untouched and records a conflict;
+ Fail (default) leaves a foreign object untouched and records a conflict,
Adopt takes ownership of an un-owned object. Adopt never steals an object
already carrying another binding's/consumer's markers.
enum:
@@ -103,7 +103,7 @@ spec:
relatedResources:
description: |-
relatedResources are Secrets/ConfigMaps synced alongside instances of the
- bound APIs. Not yet synced in the alpha POC.
+ bound APIs.
items:
description: |-
RelatedResource selects auxiliary objects (Secrets/ConfigMaps) to sync
@@ -210,7 +210,7 @@ spec:
conflictCount:
description: |-
conflictCount is the number of objects skipped due to foreign ownership.
- Per-object detail lives on each object's own condition, not here.
+ Per-object details are recorded in consumer-object annotations and Events.
format: int32
type: integer
crdHash:
diff --git a/deploy/charts/konnector-v2/files/crds/core.kbind.io_clusterbindings.yaml b/deploy/charts/konnector-v2/files/crds/core.kbind.io_clusterbindings.yaml
index 9f68aaf51..bb9561f11 100644
--- a/deploy/charts/konnector-v2/files/crds/core.kbind.io_clusterbindings.yaml
+++ b/deploy/charts/konnector-v2/files/crds/core.kbind.io_clusterbindings.yaml
@@ -63,7 +63,7 @@ spec:
description: |-
APIRef identifies an exported API by its CRD name on the provider,
i.e. "." (for example "mangodbs.mangodb.io"). Under the
- OpenAPI schema source there is no CRD object behind the name; it is still
+ OpenAPI schema source there is no CRD object behind the name, it is still
just resource + group.
properties:
name:
@@ -79,7 +79,7 @@ spec:
default: Fail
description: |-
conflictPolicy controls what happens when a target object already exists.
- Fail (default) leaves a foreign object untouched and records a conflict;
+ Fail (default) leaves a foreign object untouched and records a conflict,
Adopt takes ownership of an un-owned object. Adopt never steals an object
already carrying another binding's/consumer's markers.
enum:
@@ -101,7 +101,7 @@ spec:
relatedResources:
description: |-
relatedResources are Secrets/ConfigMaps synced alongside instances of the
- bound APIs. Not yet synced in the alpha POC.
+ bound APIs.
items:
description: |-
RelatedResource selects auxiliary objects (Secrets/ConfigMaps) to sync
@@ -208,7 +208,7 @@ spec:
conflictCount:
description: |-
conflictCount is the number of objects skipped due to foreign ownership.
- Per-object detail lives on each object's own condition, not here.
+ Per-object details are recorded in consumer-object annotations and Events.
format: int32
type: integer
crdHash:
diff --git a/deploy/charts/konnector-v2/files/crds/core.kbind.io_connections.yaml b/deploy/charts/konnector-v2/files/crds/core.kbind.io_connections.yaml
index d222a5bde..0fe509b8b 100644
--- a/deploy/charts/konnector-v2/files/crds/core.kbind.io_connections.yaml
+++ b/deploy/charts/konnector-v2/files/crds/core.kbind.io_connections.yaml
@@ -73,8 +73,7 @@ spec:
minLength: 1
type: string
namespace:
- description: namespace of the Secret. Must be the konnector's
- designated namespace.
+ description: namespace of the Secret.
minLength: 1
type: string
required:
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 000000000..5d506eddd
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,3 @@
+/.venv/
+/generated/
+/.cache/
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..e48b4c3d0
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,76 @@
+# Documentation and publishing
+
+The v2 site keeps the original MkDocs Material theme, **mike** versioning,
+and directory-based navigation using the **awesome-pages** plugin.
+Content lives under `docs/content`, each section's `.pages` file controls
+its navigation. Only this directory is published, design proposals and
+release-maintenance notes remain repository documents.
+
+## Build and preview
+
+Use Python with `venv` and the Go version from `go.mod`. CI uses Python
+3.12. From the repository root:
+
+```bash
+make docs-venv
+make build-docs
+make serve-docs
+```
+
+The first command installs dependencies into the ignored `docs/.venv`.
+`build-docs` generates references and runs a strict MkDocs build.
+`serve-docs` serves a local preview at `http://127.0.0.1:8000`, it does not
+modify any published version or redirect.
+
+The API generator scans this checkout's `sdk/apis` using a pinned
+`crd-ref-docs` version. The CLI generator uses this checkout's Cobra
+command tree. Generated references and the built site are ignored by git.
+After changing API types or CLI definitions, restart the preview or run
+`make generate-docs` again.
+
+## Non-default v2 publication
+
+The preview is always published as **`v2`**, displayed in the
+version selector as **v2 (preview)**:
+
+
+
+Publication deliberately does **not** derive the version from the newest
+tag, assign the `latest` alias, call `mike set-default`, or replace the
+root redirect. The existing 0.x versions, `latest`, and custom domain
+stay in the `gh-pages` branch.
+
+The **Docs** workflow (`.github/workflows/docs-gen-and-push.yaml`)
+builds relevant pull requests without deployment. Pushes to `v2-next`
+or the temporary `docs-v2` preparation branch, and manual dispatches on
+those branches, can publish only in `kbind-dev/kbind` once the workflow
+is committed there. Forks build but
+cannot publish to the canonical site. Publication shares the `Docs`
+concurrency group with the stable docs workflow and never force-pushes.
+
+## Publish a checkout manually
+
+Use a clean checkout containing the intended documentation, review its
+build, and confirm the selected remote is the canonical repository. For
+example, if it is named `upstream`:
+
+```bash
+git remote get-url upstream
+make docs-venv
+REMOTE=upstream make deploy-docs
+git diff --stat upstream/gh-pages..gh-pages
+git diff upstream/gh-pages..gh-pages -- versions.json index.html CNAME latest
+git push upstream gh-pages:gh-pages
+```
+
+`deploy-docs` fetches the existing publishing branch and makes a local mike
+commit. **It does not push by default.** Review that only `v2/` and the
+new `versions.json` entry changed before pushing. If someone publishes in
+the meantime, fetch and rebuild on the updated branch rather than
+force-pushing.
+
+CI passes `PUSH=1` to publish immediately after the strict build.
+`REMOTE` defaults to `origin` and `BRANCH` to `gh-pages`, override them
+only deliberately. There is intentionally no `VERSION` or default-version
+switch in the preview publisher. Making v2 the stable default is a
+separate release decision.
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index bd51cdb27..4dbb5ab0b 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -7,20 +7,30 @@ branches for future maintenance.
Releases are driven entirely by git tags. Pushing a tag that matches `v*`
triggers the [Image workflow](../.github/workflows/image.yaml), which builds the
-multi-arch `konnector` image and pushes it to the GitHub Container Registry:
+multi-arch `konnector` and `backend` images and pushes them to the GitHub
+Container Registry:
```
ghcr.io//konnector:
+ghcr.io//backend:
```
-There is no `:latest` or `:` tag — the image registry is shared with the
-v1/`main` images, so only the exact version tag is published.
+The same workflow publishes `konnector-v2` and `backend-v2` OCI Helm charts
+under `oci://ghcr.io//charts`. Their chart version drops the leading
+`v`, while `appVersion` retains it to select the matching image tag.
+
+There is no `:latest` or `:` tag, the image registry is shared with the
+0.x/`main` images, so only the exact version tag is published.
+
+The [CLI workflow](../.github/workflows/cli.yaml) publishes CLI archives and
+updates krew only for final tags. Prerelease users must build the CLI from
+the intended checkout.
Version tags follow [semantic versioning](https://semver.org):
-- `v2.0.0-rc1`, `v2.0.0-rc2`, … — release candidates (pre-releases).
-- `v2.0.0` — the final GA release.
-- `v2.0.1`, `v2.1.0`, … — patch / minor releases.
+- `v2.0.0-rc1`, `v2.0.0-rc2`, …, release candidates (pre-releases).
+- `v2.0.0`, the final GA release.
+- `v2.0.1`, `v2.1.0`, …, patch / minor releases.
## Cutting a release candidate
@@ -67,8 +77,10 @@ on your current `HEAD`, so check out the commit you intend to release first.
git push origin v2.0.0-rc1
```
-4. The Image workflow runs automatically. When it finishes, the image is
- available at `ghcr.io//konnector:v2.0.0-rc1`.
+4. The Image workflow runs automatically. When it finishes, images are
+ available at `ghcr.io//{konnector,backend}:v2.0.0-rc1`, and OCI
+ charts at `oci://ghcr.io//charts/{konnector-v2,backend-v2}`
+ with version `2.0.0-rc1`.
5. (Optional) Create a GitHub Release from the tag and mark it as a
pre-release:
@@ -140,6 +152,12 @@ gh release create v2.0.1 --generate-notes
## Naming conventions
+Documentation publication is separate from binary releases. The
+[v2 preview publisher](README.md) deploys
+`v2` without moving `latest` or the site's default redirect, including
+when v2 release-candidate tags exist. Promoting v2 to the default docs
+requires a separate, explicit change.
+
| Kind | Example | Cut from |
|------------------|----------------|-----------------|
| Release candidate| `v2.0.0-rc1` | `v2-next` |
diff --git a/docs/content/.pages b/docs/content/.pages
new file mode 100644
index 000000000..eb74d4c17
--- /dev/null
+++ b/docs/content/.pages
@@ -0,0 +1,10 @@
+nav:
+ - Home:
+ - index.md
+ - Community: community
+ - Setup: setup
+ - Usage: usage
+ - Contributing: contributing
+ - Developers: developers
+ - API References: reference
+ - Blog: blog
diff --git a/docs/content/blog/.authors.yml b/docs/content/blog/.authors.yml
new file mode 100644
index 000000000..481c7f08e
--- /dev/null
+++ b/docs/content/blog/.authors.yml
@@ -0,0 +1,5 @@
+authors:
+ olamilekan000:
+ name: Olalekan Odukoya
+ description: Contributor
+ avatar: https://avatars.githubusercontent.com/u/24735571?v=4
diff --git a/docs/content/blog/index.md b/docs/content/blog/index.md
new file mode 100644
index 000000000..c58f16c50
--- /dev/null
+++ b/docs/content/blog/index.md
@@ -0,0 +1,2 @@
+# Blog
+
diff --git a/docs/content/blog/posts/2026-02-14-kube-bind-internals.md b/docs/content/blog/posts/2026-02-14-kube-bind-internals.md
new file mode 100644
index 000000000..5a2519296
--- /dev/null
+++ b/docs/content/blog/posts/2026-02-14-kube-bind-internals.md
@@ -0,0 +1,424 @@
+---
+title: "Deep Dive: Understanding kube-bind Internals"
+description: "A comprehensive look at how kube-bind works under the hood, covering the Service Provider, Service Consumer, and Konnector architecture."
+icon: material/engine
+date: 2026-02-14
+authors:
+ - olamilekan000
+---
+
+# Deep Dive: Understanding kube-bind Internals
+
+!!! note "Historical 0.x article"
+ This article is preserved from the 0.x documentation. Its commands and API names are not v2 instructions, use the [v2 quickstart](../../setup/quickstart.md).
+
+> **Note:** Just want to get started quickly? Check out our [Quick Start Guide](2026-02-14-kube-bind-quickstart.md) which sets up everything automatically with `kubectl bind dev create`. This article explains the machinery under the hood, the "Hard Way".
+
+If you’ve ever tried to make one cluster consume resources from another, you’ve probably had to deal with complicated networking setups, VPN tunnels, duplicated Custom Resource Definitions (CRDs), or custom-built controllers to keep everything in sync.
+
+As organizations grow, the need for multi-cluster setups become inevitable, and this comes with its own headache especially when you need to share services or resource between clusters. Doing this in Kubernetes is inherently hard because clusters are isolated by design. They don’t natively “talk” to each other.
+
+This is where kube-bind comes in.
+
+
+
+To understand kube-bind, it helps to think about how mobile apps work.
+
+When a developer builds an app, they don’t send the source code directly to every user. Instead, they publish it to an app marketplace like the App Store. Users can then browse, install, and use the app without needing direct access to the developer’s system.
+
+kube-bind works in a similar way.
+
+One Kubernetes cluster can “publish” selected APIs, and another cluster can “install” or bind to them. The consumer cluster doesn’t need full access to the provider cluster, it only interacts with the exported APIs, just like installing an app.
+
+kube-bind provides a Kubernetes-native way to securely export APIs from one cluster and bind to them in another without stitching clusters together through complex networking configurations or manual synchronization.
+
+In this article, we’ll explore what kube-bind is, why it exists, and how to use it step-by-step to share services across Kubernetes clusters.
+
+## What Is kube-bind?
+
+`kube-bind` is an open-source tool that allows you to export Kubernetes APIs from a **Service Provider** cluster and consume them in a **Consumer** cluster.
+
+It effectively "projects" an API (like a CRD) from one cluster to another. To the consumer, it feels like the resource is local. They can create a `PostgreSQL` object in their namespace and interact with it just like any other Kubernetes resource, however, the actual logic and heavy operational workload happens on the provider's side.
+
+```mermaid
+graph LR
+ subgraph Provider Cluster
+ A[PostgreSQL Controller] --> B[Actual Postgres DB]
+ C[APIServiceExport]
+ end
+
+ subgraph Consumer Cluster
+ D[Konnector]
+ E[Shadow CRD]
+ end
+
+ D <-->|Syncs Resources| C
+ E -.->|User Creates Resource| D
+```
+
+> `kube-bind` focuses on **service binding**. It does not merge or federate control planes. Instead, it allows one cluster to securely consume specific APIs exported by another.
+
+## Core Concepts & API Objects
+
+Under the hood, `kube-bind` uses a set of Custom Resource Definitions (CRDs) to manage the lifecycle of exported services and bindings. Here are the key objects you'll encounter:
+
+- **`APIServiceExport`**: Represents a specific API (CRD) that the provider wants to export. An export makes the API available to bind.
+- **`APIServiceExportTemplate`**: A template used to instantiate the `APIServiceExport`. It defines what resources can be exported and carries configuration for the export.
+- **`ClusterBinding`**: A resource on the consumer side that represents the connection to a specific provider. It holds the authentication details and endpoint information.
+- **`APIServiceBinding`**: A resource on the consumer side that represents a binding to a specific exported API. When created, it triggers the synchronization process.
+- **`Konnector`**: The lightweight agent running in the consumer cluster. It watches `APIServiceBinding` resources and handles the actual synchronization of data between the consumer and provider.
+
+## The "Hard Way": Manual Setup & Architecture
+
+To really understand how `kube-bind` works, we're going to set it up manually. This is what the `dev create` command does automatically, but looking at the individual steps reveals the architecture.
+
+Let's see this in action. We'll simulate both the provider and consumer on your local machine.
+
+### Prerequisites
+
+You'll need:
+
+- [kind](https://kind.sigs.k8s.io/)
+- [kubectl](https://kubernetes.io/docs/tasks/tools/)
+- [kube-bind CLI](https://github.com/kube-bind/kube-bind/releases)
+
+### Install kube-bind
+
+The easiest way to install the `kube-bind` CLI is via [krew](https://krew.sigs.k8s.io/), the plugin manager for `kubectl`. Since `kube-bind` is in its own index, you need to add it first:
+
+```bash
+kubectl krew index add bind https://github.com/kube-bind/krew-index.git
+kubectl krew install bind/bind
+```
+
+You should see output similar to this:
+
+```text
+➜ kubectl krew index add bind https://github.com/kube-bind/krew-index.git
+WARNING: You have added a new index from "https://github.com/kube-bind/krew-index.git"
+The plugins in this index are not audited for security by the Krew maintainers.
+Install them at your own risk.
+
+➜ kubectl krew install bind/bind
+Updated the local copy of plugin index "bind".
+Installing plugin: bind
+Installed plugin: bind
+\
+ | Use this plugin:
+ | kubectl bind
+ | Documentation:
+ | https://kube-bind.io/
+/
+```
+
+If you don't have krew installed, you can download the binary directly from the [releases page](https://github.com/kube-bind/kube-bind/releases).
+
+### Step 1: Create the Clusters
+
+We need at least two clusters, but first, we'll start by creating a **provider** cluster.
+
+**Create the provider cluster with port mapping:**
+
+```bash
+cat <> /etc/hosts"
+```
+
+**Windows**: Add `127.0.0.1 kube-bind-backend.kube-bind.svc` to `C:\Windows\System32\drivers\etc\hosts`.
+
+Now, install the backend using Helm, configuring it to use this domain:
+
+```bash
+kubectl config use-context kind-provider
+
+# Install the backend using Helm
+helm upgrade --install \
+ --namespace kube-bind \
+ --create-namespace \
+ --set image.tag=v0.7.1 \
+ --set backend.externalAddress=https://provider-control-plane:6443 \
+ --set backend.tlsExternalServerName=kubernetes.default.svc \
+ --set backend.oidc.issuerUrl=http://kube-bind-backend.kube-bind.svc:8080/oidc \
+ --set backend.oidc.callbackUrl=http://kube-bind-backend.kube-bind.svc:8080/api/callback \
+ kube-bind oci://ghcr.io/kbind-dev/charts/backend --version 0.7.1
+```
+
+> **Note:** The OIDC configuration used here (which uses the built-in mock OIDC provider of `kube-bind-backend`) is for demonstration purposes only. For a production deployment, you must integrate with a real OIDC provider. See [Installation with Helm](../../setup/helm.md) for production guidelines.
+
+> **Important Configuration Details:**
+>
+> - `backend.externalAddress=https://provider-control-plane:6443` - This is the address the Konnector will use to connect to the provider's API server.
+> - `backend.tlsExternalServerName=kubernetes.default.svc` - This ensures TLS verification succeeds even though we're connecting via a custom hostname.
+> - `backend.oidc.callbackUrl` - Must include the `http://` scheme to ensure proper browser redirects during authentication.
+
+After installing, verify that the backend is running:
+
+```bash
+kubectl get po -n kube-bind
+```
+
+You should see something like this:
+
+```text
+NAME READY STATUS RESTARTS AGE
+kube-bind-backend-6779b5b99f-vczmq 1/1 Running 0 18m
+```
+
+Since we are running locally in Kind without an Ingress, we need to port-forward the backend service.
+
+**Open a new terminal window** and run:
+
+```bash
+kubectl port-forward svc/kube-bind-backend -n kube-bind 8080:8080 --context kind-provider
+```
+
+ _If you visit `http://kube-bind-backend.kube-bind.svc:8080` in your browser, you'll see this screen. This is normal and confirms the backend is running!_
+
+### Step 3: Exporting an API Service
+
+Now that the backend is running, we can export an API. We'll use the **Cowboy** CRD included in the `kube-bind` repository as our example.
+
+First, apply the CRD to the **provider** cluster:
+
+```bash
+kubectl apply -f https://raw.githubusercontent.com/kube-bind/kube-bind/main/deploy/examples/crd-cowboys.yaml
+```
+
+Verify that the CRD is installed:
+
+```bash
+kubectl get crd
+```
+
+You should see `cowboys.wildwest.dev` in the list:
+
+```text
+NAME CREATED AT
+apiserviceexports.kube-bind.io 2026-02-14T13:04:56Z
+...
+cowboys.wildwest.dev 2026-02-14T13:57:13Z
+```
+
+Now, we need to **export** this service so consumers can find it. We do this by creating an `APIServiceExportTemplate`. This tells the backend to list "Cowboys" in its service catalog.
+
+```bash
+kubectl apply -f https://raw.githubusercontent.com/kube-bind/kube-bind/main/deploy/examples/template-cowboys.yaml
+```
+
+Verify that the template is created:
+
+```bash
+kubectl get apiserviceexporttemplates
+```
+
+Output:
+
+```text
+NAME RESOURCES PERMISSIONCLAIMS AGE
+cowboys wildwest.dev secrets 12s
+```
+
+The service is now exported!
+
+### Step 4: Bind the Consumer
+
+Switch to the **consumer** cluster.
+
+```bash
+kubectl config use-context kind-consumer
+```
+
+Run the login command to authenticate with the provider:
+
+```bash
+kubectl bind login http://kube-bind-backend.kube-bind.svc:8080
+```
+
+You should see output similar to:
+
+```text
+Connecting to kube-bind server http://kube-bind-backend.kube-bind.svc:8080...
+Started local callback server at http://127.0.0.1:64184/callback
+Opening browser for authentication...
+🔑 Successfully authenticated to kube-bind-backend.kube-bind.svc:8080
+Configuration saved to: /Users/username/.kube-bind/config
+```
+
+Now run the bind command:
+
+```bash
+kubectl bind http://kube-bind-backend.kube-bind.svc:8080
+```
+
+This will open your browser showing the available services in the Provider's template catalog. Amongst them, you'll see the **Cowboys** template that we exported earlier.
+
+
+
+Click **"Bind for CLI"** to proceed. After clicking, your browser will show a success message:
+
+
+
+Meanwhile, back in your terminal, you'll see the CLI deploying the Konnector:
+
+```text
+🌐 Opening kube-bind UI in your browser...
+Browser opened successfully
+Waiting for binding completion from UI...
+ (Press Ctrl+C to cancel)
+
+Binding completed successfully!
+Created kube-bind namespace.
+🔒 Created secret kube-bind/kubeconfig-9lbzx for host https://provider-control-plane:6443, namespace kube-bind-enkvby5uzkct
+🚀 Deploying konnector v0.7.1 to namespace kube-bind.
+ Waiting for the konnector to be ready.................
+✅ Created APIServiceBinding cowboys for 1 resources
+Created 1 APIServiceBinding(s):
+ - cowboys
+Resources bound successfully!
+```
+
+Behind the scenes, the CLI created a `kube-bind` namespace, an `APIServiceBinding`, deploys the `konnector` and creates some other resource in the consumer cluster. The `APIServiceBinding` resource tells the Konnector which services to sync from the provider.
+
+Check available namespaces
+
+```bash
+kubectl get ns
+```
+
+You should see the `kube-bind` namespace:
+
+```text
+NAME STATUS AGE
+default Active 6h6m
+kube-bind Active 92s
+kube-node-lease Active 6h6m
+kube-public Active 6h6m
+kube-system Active 6h6m
+local-path-storage Active 6h6m
+```
+
+Check the Konnector pods:
+
+```bash
+kubectl get po -n kube-bind
+```
+
+Output: We can see the konnector pods running in the `kube-bind` namespace.
+
+```text
+NAME READY STATUS RESTARTS AGE
+konnector-547ff86976-5rmh8 1/1 Running 0 98s
+konnector-547ff86976-xkcdm 1/1 Running 0 98s
+```
+
+### Step 5: Verify and Use
+
+After the binding completes, several CRDs will be installed in your consumer cluster. Let's verify:
+
+```bash
+kubectl get crd
+```
+
+You should see the kube-bind infrastructure CRDs along with the Cowboys CRD:
+
+```text
+NAME CREATED AT
+apiservicebindingbundles.kube-bind.io 2026-02-14T23:37:29Z
+apiservicebindings.kube-bind.io 2026-02-14T23:37:29Z
+cowboys.wildwest.dev 2026-02-14T23:37:37Z
+```
+
+The `cowboys.wildwest.dev` CRD is now available locally! The other CRDs are part of the kube-bind infrastructure that manages the binding lifecycle.
+
+Check that the **Cowboy** CRD is available:
+
+```bash
+kubectl get crd cowboys.wildwest.dev
+```
+
+Now, you can create a `Cowboy` resource directly in your consumer cluster. The magic is that this resource will be managed by the provider!
+
+```bash
+kubectl apply -f - < ProviderAPI
+ end
+
+ subgraph Consumer["Consumer Cluster"]
+ Konnector["Konnector Agent"]
+ BoundAPI["MangoDB CRD Synced Copy"]
+ Konnector --> BoundAPI
+ end
+
+ User -->|"1. kubectl bind dev create"| CLI
+ CLI -.->|"Creates & Installs"| Backend
+ CLI -.->|"Creates Cluster"| Consumer
+
+ User -->|"2. kubectl bind login"| Backend
+ Backend -->|"Auth Token"| User
+
+ User -->|"3. kubectl bind create Select API in UI"| CLI
+ CLI -->|"Install Konnector"| Konnector
+
+ Konnector <-->|"4. Syncs Resources"| Backend
+
+ User -.->|"5. kubectl apply/get"| BoundAPI
+ BoundAPI -.->|"Synced to"| ProviderAPI
+```
+
+
+
+In this guide, we'll get you up and running with `kube-bind` in minutes. We'll use the `dev create` command to automatically provision a local playground with a Provider and Consumer cluster.
+
+> **Note:** If you want to understand how `kube-bind` works internally or set it up manually for production, check out our deep dive: [Understanding kube-bind Internals](2026-02-14-kube-bind-internals.md).
+
+## Prerequisites
+
+You'll need:
+
+- [Docker](https://www.docker.com/) running.
+- [kubectl](https://kubernetes.io/docs/tasks/tools/).
+- `kube-bind` CLI installed.
+
+### Install kube-bind
+
+The easiest way is via [krew](https://krew.sigs.k8s.io/):
+
+```bash
+kubectl krew index add bind https://github.com/kube-bind/krew-index.git
+kubectl krew install bind/bind
+```
+
+## Step 1: Create the Environment
+
+Run the following command to spin up the entire demo environment:
+
+```bash
+kubectl bind dev create
+```
+
+> **Note:** The `dev create` command sets up a mock OIDC provider for testing purposes. This is **not** suitable for production. For a production-ready deployment with real OIDC integration, please refer to our [Installation with Helm](../../setup/helm.md) guide.
+
+This will:
+
+1. Create a **Provider** cluster (`kind-provider`).
+2. Create a **Consumer** cluster (`kind-consumer`).
+3. Install the `kube-bind-backend` on the provider.
+4. Configure networking and OIDC mock services.
+
+Once it finishes, you'll see output like this:
+
+```text
+kube-bind Development Environment Setup
+
+EXPERIMENTAL: kube-bind dev command is in preview
+Requirements: Docker must be installed and running
+
+Warning: Could not automatically add host entry. Please run:
+ echo '127.0.0.1 kube-bind.dev.local' | sudo tee -a /etc/hosts
+
+Creating kind cluster kind-provider with network kube-bind-dev
+Kind cluster kind-provider created
+Helm chart installed successfully
+
+Creating kind cluster kind-consumer with network kube-bind-dev
+Kind cluster kind-consumer created
+kube-bind dev environment is ready!
+
+Configuration:
+• Provider cluster kubeconfig: kind-provider.kubeconfig
+• Consumer cluster kubeconfig: kind-consumer.kubeconfig
+• kube-bind server URL: http://kube-bind.dev.local:8080
+
+Next Steps:
+
+1. Add to /etc/hosts (if not already done):
+echo '127.0.0.1 kube-bind.dev.local' | sudo tee -a /etc/hosts
+
+2. Login to authenticate to the provider cluster:
+kubectl bind login http://kube-bind.dev.local:8080
+
+3. Bind an API service from provider to consumer:
+PROVIDER_IP=$(docker inspect kind-provider-control-plane | jq -r '.[0].NetworkSettings.Networks["kube-bind-dev"].IPAddress') && KUBECONFIG=kind-consumer.kubeconfig kubectl bind --konnector-host-alias ${PROVIDER_IP}:kube-bind.dev.local
+```
+
+## Step 2: Authenticate
+
+Copy and run the login command provided in the output:
+
+```bash
+kubectl bind login http://kube-bind.dev.local:8080
+```
+
+Output:
+
+```text
+Connecting to kube-bind server http://kube-bind.dev.local:8080...
+Started local callback server at http://127.0.0.1:56642/callback
+Opening browser for authentication...
+🔑 Successfully authenticated to kube-bind.dev.local:8080
+Configuration saved to: /Users/olalekanodukoya/.kube-bind/config
+```
+
+This will open your browser to authenticate. Since this is a dev environment, just follow the prompts.
+
+## Step 3: Bind a Service
+
+Now, switch to the consumer role and bind to a service. Copy the second command from the `dev create` output. It will look something like this:
+
+```bash
+PROVIDER_IP=$(docker inspect kind-provider-control-plane | jq -r '.[0].NetworkSettings.Networks["kube-bind-dev"].IPAddress') && KUBECONFIG=kind-consumer.kubeconfig kubectl bind --konnector-host-alias ${PROVIDER_IP}:kube-bind.dev.local
+```
+
+This will open a web interface where you can choose which API to bind. Select **mangodb** and click **Bind for CLI**.
+
+
+
+Output:
+
+```text
+🌐 Opening kube-bind UI in your browser...
+Browser opened successfully
+Waiting for binding completion from UI...
+ (Press Ctrl+C to cancel)
+
+Binding completed successfully!
+Created kube-bind namespace.
+🔒 Created secret kube-bind/kubeconfig-tjm2k for host https://kube-bind.dev.local:6443, namespace kube-bind-3iwzhtescg5o0
+🚀 Deploying konnector v0.7.0 to namespace kube-bind.
+ Waiting for the konnector to be ready.................
+✅ Created APIServiceBinding mangodb for 1 resources
+Created 1 APIServiceBinding(s):
+ - mangodb
+Resources bound successfully!
+```
+
+You can verify the new CRDs are available:
+
+```bash
+kubectl get crd
+```
+
+Output:
+
+```text
+NAME CREATED AT
+apiservicebindingbundles.kube-bind.io 2026-02-17T18:50:02Z
+apiservicebindings.kube-bind.io 2026-02-17T18:50:02Z
+mangodbs.mangodb.com 2026-02-17T18:50:13Z
+```
+
+## Step 4: Use the Service
+
+Once the binding is complete, you can interact with the `MangoDB` resource directly from your consumer cluster!
+
+```bash
+export KUBECONFIG=kind-consumer.kubeconfig
+
+# Create a MangoDB resource
+kubectl apply -f - <
+
+[Keycloak](https://www.keycloak.org/) is one of the most widely deployed open-source identity solutions out there. It supports OpenID Connect (OIDC), has a nice admin UI, and is battle-tested. Since `kube-bind` speaks OIDC, Keycloak is a very natural fit.
+
+Here is what we'll build:
+
+```mermaid
+graph TB
+ User((You / Consumer))
+
+ subgraph Provider["Provider Cluster"]
+ Backend["kube-bind Backend"]
+ Keycloak["Keycloak OIDC Provider"]
+ ProviderAPI["Exported API (CRD)"]
+ end
+
+ subgraph Consumer["Consumer Cluster"]
+ Konnector["Konnector Agent"]
+ BoundCRD["Bound CRD (local copy)"]
+ end
+
+ User -->|"1. kubectl bind login"| Backend
+ Backend -->|"2. Redirects to Keycloak"| Keycloak
+ Keycloak -->|"3. Issues token on login"| Backend
+ Backend -->|"4. Session established"| User
+
+ User -->|"5. kubectl bind"| Backend
+ Backend -->|"6. Install Konnector"| Konnector
+ Konnector <-->|"7. Syncs resources"| ProviderAPI
+ Konnector --> BoundCRD
+```
+
+The key difference from the dev setup is steps 2 and 3, instead of clicking through a mock login screen, users are redirected to Keycloak's real login page. `kube-bind` validates the resulting tokens against Keycloak's OIDC discovery endpoint.
+
+## Prerequisites
+
+You'll need:
+
+- [kind](https://kind.sigs.k8s.io/)
+- [kubectl](https://kubernetes.io/docs/tasks/tools/)
+- [Helm](https://helm.sh/) 3.x
+- The `kube-bind` CLI, see [Quick Start](2026-02-14-kube-bind-quickstart.md) for installation
+
+> **Note:** We'll use port-forwarding here to keep things simple. A production setup would use a proper Ingress or Gateway with TLS. See [Installation with Helm](../../setup/helm.md) for that.
+
+---
+
+## Step 1: Create the Provider Cluster
+
+We need a Kind cluster with port mappings so we can reach both Keycloak and the kube-bind backend from our machine:
+
+```bash
+cat < **Why the certSANs?** Since we are exposing the API server on port `6443` and using custom hostnames like `provider-control-plane`, we need to tell Kind to include these in its TLS certificates. Without this, both **kubectl** and the **kube-bind CLI** would reject the connection with a certificate validation error.
+
+Then add some hostname aliases so we can refer to our services by name:
+
+```bash
+sudo sh -c "echo '127.0.0.1 kube-bind.local keycloak.local provider-control-plane' >> /etc/hosts"
+```
+
+> [!TIP]
+> **Fixing "Certificate is valid for..." Errors:** If you see an error like `x509: certificate is valid for ..., not 0.0.0.0`, it's because `kubectl` is trying to connect via an IP that is not in the certificate. You can fix your current context instantly by running:
+>
+> ```bash
+> kubectl config set-cluster kind-provider --server=https://127.0.0.1:6443
+> ```
+
+---
+
+## Step 2: Deploy Keycloak
+
+We'll use the **KeycloakX** Helm chart from [codecentric](https://github.com/codecentric/helm-charts). This is a great community-maintained chart that uses the official Keycloak images.
+
+Because we need to set some environment variables for the admin user, we'll create a small `values-keycloak.yaml` file. This is much cleaner than trying to pass complex strings via the CLI.
+
+```bash
+cat < values-keycloak.yaml
+fullnameOverride: keycloak
+command:
+ - "/opt/keycloak/bin/kc.sh"
+ - "start-dev"
+extraEnv: |
+ - name: KEYCLOAK_ADMIN
+ value: admin
+ - name: KEYCLOAK_ADMIN_PASSWORD
+ value: admin123
+ - name: KC_HTTP_RELATIVE_PATH
+ value: /auth
+EOF
+
+helm repo add codecentric https://codecentric.github.io/helm-charts
+helm repo update
+
+helm upgrade --install keycloak codecentric/keycloakx \
+ --namespace keycloak \
+ --create-namespace \
+ -f values-keycloak.yaml
+```
+
+You should see the following output:
+
+```text
+Release "keycloak" does not exist. Installing it now.
+NAME: keycloak
+LAST DEPLOYED: Sun Feb 22 21:52:49 2026
+NAMESPACE: keycloak
+STATUS: deployed
+REVISION: 1
+TEST SUITE: None
+NOTES:
+***********************************************************************
+* *
+* Keycloak.X Helm Chart by codecentric AG *
+* *
+***********************************************************************
+
+Keycloak was installed with a Service of type ClusterIP
+
+Create a port-forwarding with the following commands:
+
+export POD_NAME=$(kubectl get pods --namespace keycloak -l "app.kubernetes.io/name=keycloakx,app.kubernetes.io/instance=keycloak" -o name)
+echo "Visit http://127.0.0.1:8080 to use your application"
+kubectl --namespace keycloak port-forward "$POD_NAME" 8080
+```
+
+Wait for it to come up (Heads up: this chart deploys a **StatefulSet**, not a Deployment):
+
+```bash
+kubectl rollout status statefulset/keycloak -n keycloak --timeout=120s
+```
+
+Patch the service to expose port 8443 for OIDC compatibility since we're technically running the cluster in our local machine.
+
+```bash
+kubectl patch service keycloak-http -n keycloak --type='json' -p='[
+ {"op": "replace", "path": "/spec/ports/2/port", "value": 8444},
+ {"op": "add", "path": "/spec/ports/-", "value": {"name": "oidc-compat", "port": 8443, "targetPort": "http", "protocol": "TCP"}}
+]'
+```
+
+Port-forward so we can access the admin console:
+
+```bash
+kubectl port-forward svc/keycloak-http -n keycloak 8443:80 &
+```
+
+Head over to [http://keycloak.local:8443](http://keycloak.local:8443). You'll be redirected to the Keycloak login page:
+
+
+
+Log in with `admin` / `admin123`. You should see the Keycloak admin console.
+
+---
+
+## Step 3: Configure Keycloak
+
+This is the most important step. We need to set up a few things in Keycloak:
+
+1. A **Realm**, a logical namespace for our identity space
+2. A **Client**, representing the `kube-bind` backend
+3. A **Group**, to control which users can bind services
+4. A **User**, so we have someone to log in as
+
+### Create a Realm
+
+In the top-left corner of the admin console, you'll see a **Manage realms** option, click it. You'll see a list of realms, one of which is named `master`. We won't be using that one. Click on the **Create realm** button.
+
+
+
+Name the new realm `kube-bind` and click **Create**.
+
+### Create a Client
+
+This is the entry for the `kube-bind` backend application in Keycloak.
+
+Go to **Clients** → **Create client** and fill in the following:
+
+- **Client ID**: `kube-bind`
+- **Client type**: `OpenID Connect`
+
+Click **Next**. On the capability config screen:
+
+- Enable **Client authentication** (this generates a client secret)
+- Keep **Standard flow** checked
+
+
+
+Click **Next**. On the login settings screen:
+
+- **Valid redirect URIs**: `http://kube-bind.local:8080/api/callback`
+- **Web origins**: `http://kube-bind.local:8080`
+
+
+
+Click **Save**.
+
+### Retrieve the Client Secret
+
+Now that the client is created, we need its secret to give to the `kube-bind` backend.
+
+1. Go to the **Credentials** tab at the top.
+2. Under **Client Secret**, click the copy icon to copy the value to your clipboard.
+
+
+
+> **Note:** If you don't see the **Credentials** tab, make sure **Client authentication** is toggled to **ON** in the client's **Settings** tab.
+
+### Create a Group
+
+`kube-bind` lets you restrict who can create bindings based on OIDC group membership. Let's set that up.
+
+Go to **Groups** → **Create group**, name it `kube-bind-users`, and click **Create**.
+
+### Create a Test User
+
+Go to **Users** → **Create new user** and fill in a **Username** (e.g., `platform-operator`). Toggle **Email verified** to on, then click **Create**.
+
+
+
+Go to the **Credentials** tab → **Set password** (something like `password123`, and disable temporary). Then go to the **Groups** tab → **Join group** → select `kube-bind-users`.
+
+> **Technical Note on Groups:** The `kube-bind` backend is hardcoded to look for a claim named `groups` in your OIDC token. This is why the **Token Claim Name** in the Keycloak mapper MUST be exactly `groups`. If you use a different name, the backend won't "see" your group membership even if you are in the right group!
+
+### Configure Client Scopes
+
+Keycloak won't grant scopes like `profile` or `groups` unless they are explicitly assigned to the client.
+
+#### 1. Create the 'groups' Scope
+
+Instead of adding a mapper directly to the client, we'll create a reusable scope:
+
+1. In the sidebar, click **Client scopes** → **Create client scope**.
+2. **Name**: `groups`
+3. Click **Save**.
+4. Go to the **Mappers** tab → **Configure a new mapper** → **Group Membership**.
+5. **Name**: `groups`, **Token Claim Name**: `groups`.
+6. Ensure **Full group path** is `OFF`.
+7. Click **Save**.
+
+
+
+#### 2. Assign Scopes to the Client
+
+Now, we must tell the `kube-bind` client to actually use these scopes. Some of these are built-in to Keycloak, while `groups` is the one we just created:
+
+1. Go to **Clients** → `kube-bind` → **Client scopes** tab.
+2. Click **Add client scope**.
+3. Select `profile`, `email`, and `offline_access` (these are built-in and already exist).
+4. Also select the `groups` scope you just created.
+
+> **Check the pagination!** Keycloak's "Add client scope" dialog is paginated. If you don't see `profile` or `email` on the first page, click the arrow at the bottom right to go to the next page.
+
+5. Click **Add** and choose **Default**.
+
+
+
+That's Keycloak done! The `kube-bind` backend will now be able to request and receive these scopes.
+
+---
+
+## Step 4: Deploy kube-bind
+
+Keycloak's OIDC issuer URL follows this pattern: `http:///auth/realms/`. For us, that's `http://keycloak.local:8443/auth/realms/kube-bind`.
+
+```bash hl_lines="4 15"
+kubectl config use-context kind-provider
+
+helm upgrade --install kube-bind \
+ --namespace kube-bind \
+ --create-namespace \
+ --set image.tag=v0.7.1 \
+ --set backend.externalAddress=https://provider-control-plane:6443 \
+ --set backend.tlsExternalServerName=kubernetes.default.svc \
+ --set backend.oidc.type=external \
+ --set backend.oidc.issuerUrl=http://keycloak.local:8443/auth/realms/kube-bind \
+ --set backend.oidc.clientId=kube-bind \
+ --set backend.oidc.clientSecret=cSmfhB3RNuetE8pgz1hDVjDHsDpc2r2v\
+ --set backend.oidc.callbackUrl=http://kube-bind.local:8080/api/callback \
+ --set backend.oidc.allowedGroups={kube-bind-users} \
+ oci://ghcr.io/kbind-dev/charts/backend --version 0.7.1
+```
+
+Replace `` with what you copied from the Keycloak credentials tab.
+
+> **Heads up:** Never paste secrets directly into shell history in production. Use a Kubernetes Secret or an environment variable via `--set backend.oidc.clientSecret=$OIDC_CLIENT_SECRET`.
+
+### Networking Adjustment (Kind Specific)
+
+Since we are running in Kind, the `kube-bind` pod doesn't know about the `keycloak.local` entry on your host's `/etc/hosts` file. Without this next step, the backend will try to reach itself on `127.0.0.1:8443` and fail.
+
+We need to tell the pod that `keycloak.local` is actually the IP of our Keycloak Service.
+
+```bash
+# 1. Get the ClusterIP of the Keycloak service
+KEYCLOAK_IP=$(kubectl get svc keycloak-http -n keycloak -o jsonpath='{.spec.clusterIP}')
+
+# 2. Patch the backend to add a hostAlias
+kubectl patch deployment kube-bind-backend -n kube-bind --type='json' -p="[
+ {\"op\": \"add\", \"path\": \"/spec/template/spec/hostAliases\", \"value\": [
+ {\"ip\": \"$KEYCLOAK_IP\", \"hostnames\": [\"keycloak.local\"]}
+ ]}
+]"
+```
+
+Verify everything is running:
+
+```bash
+kubectl get po -n kube-bind
+```
+
+```text
+NAME READY STATUS RESTARTS AGE
+kube-bind-backend-67b5bc9768-l9vr4 1/1 Running 0 45s
+```
+
+Port-forward the backend:
+
+```bash
+kubectl port-forward svc/kube-bind-backend -n kube-bind 8080:8080 &
+```
+
+---
+
+## Step 5: Export an API
+
+Now let's give the consumer something to bind to. We'll use the **Cowboys** example CRD that ships with `kube-bind`:
+
+```bash
+kubectl apply -f https://raw.githubusercontent.com/kube-bind/kube-bind/main/deploy/examples/crd-cowboys.yaml
+kubectl apply -f https://raw.githubusercontent.com/kube-bind/kube-bind/main/deploy/examples/template-cowboys.yaml
+```
+
+Verify it's exported:
+
+```bash
+kubectl get apiserviceexporttemplates
+```
+
+```text
+NAME RESOURCES PERMISSIONCLAIMS AGE
+cowboys wildwest.dev secrets 8s
+```
+
+---
+
+## Step 6: Bind from the Consumer
+
+Create the consumer cluster:
+
+```bash
+kind create cluster --name consumer
+kubectl config use-context kind-consumer
+```
+
+Run the login command:
+
+```bash
+kubectl bind login http://kube-bind.local:8080
+```
+
+Your browser will open, and instead of the mock login page, you'll see **Keycloak's real login form**. Log in with `platform-operator` and the password you set earlier.
+
+
+
+> On your first login, Keycloak might ask you to **Update Account Information** (First name, Last name, and Email). This is a standard "Required Action" for new users. Just fill in the details and click **Submit**.
+>
+> 
+>
+> Once you submit your information, you'll see the classic `kube-bind` success page:
+>
+> 
+
+```text
+Connecting to kube-bind server http://kube-bind.local:8080...
+Started local callback server at http://127.0.0.1:59371/callback
+Opening browser for authentication...
+🔑 Successfully authenticated to kube-bind.local:8080
+Configuration saved to: /Users/olalekanodukoya/.kube-bind/config
+```
+
+Now bind the API:
+
+```bash
+kubectl bind http://kube-bind.local:8080
+```
+
+The CLI will once again open your browser to confirm the binding. Since you just logged in, Keycloak's SSO will likely skip the username/password prompt and take you straight to the resource selection screen.
+
+Select **Cowboys** in the browser UI and click **Bind for CLI**.
+
+
+
+```text
+🌐 Opening kube-bind UI in your browser...
+ http://kube-bind.local:8080?consumer_id=ef0dc4ce-c867-48eb-ba6d-94178eefeca5&redirect_url=http%3A%2F%2F127.0.0.1%3A53613%2Fcallback&session_id=OV4RLFOX2V7HXSTY5FFJFGF4BX
+
+Browser opened successfully
+Waiting for binding completion from UI...
+ (Press Ctrl+C to cancel)
+
+Binding completed successfully!
+Created kube-bind namespace.
+🔒 Created secret kube-bind/kubeconfig-2gf2s for host https://provider-control-plane:6443, namespace kube-bind-4fx0okhmvche
+🚀 Deploying konnector v0.7.1 to namespace kube-bind.
+ Waiting for the konnector to be ready.............
+✅ Created APIServiceBinding cowboys for 1 resources
+Created 1 APIServiceBinding(s):
+ - cowboys
+Resources bound successfully!
+
+```
+
+---
+
+## Step 7: Verify It Works
+
+Check that the CRD is now available in the consumer:
+
+```bash
+kubectl get crd cowboys.wildwest.dev
+```
+
+Create a resource:
+
+```bash
+kubectl apply -f - <
+
+
+## Governance and contributing
+
+- [Contributing guide](../contributing/index.md)
+- [Project governance](https://github.com/kbind-dev/kbind/blob/main/GOVERNANCE.md)
+- [Code of conduct](https://github.com/kbind-dev/kbind/blob/main/code-of-conduct.md)
diff --git a/docs/content/contributing/index.md b/docs/content/contributing/index.md
new file mode 100644
index 000000000..03cf70a6a
--- /dev/null
+++ b/docs/content/contributing/index.md
@@ -0,0 +1,54 @@
+# Contributing to kbind
+
+kbind is [Apache 2.0 licensed](https://github.com/kbind-dev/kbind/tree/main/LICENSE) and we accept contributions via GitHub pull requests.
+
+Please read the following guide if you're interested in contributing to kube-bind.
+
+## Certificate of Origin
+
+By contributing to this project you agree to the Developer Certificate of Origin (DCO). This document was created by the Linux Kernel community and is a simple statement that you, as a contributor, have the legal right to make the contribution. See the [DCO](https://github.com/kbind-dev/kbind/tree/main/DCO) file for details.
+
+## Getting Started
+
+### Prerequisites
+
+1. Clone this repository.
+2. [Install Go](https://golang.org/doc/install) using the version declared in `go.mod`.
+3. Install [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl).
+
+More on the development environment setup can be found in the [developer guide](../developers/dev-environment/index.md).
+
+## Finding Areas to Contribute
+
+Starting to participate in a new project can sometimes be overwhelming, and you may not know where to begin. Fortunately, we are here to help! We track all of our tasks here in GitHub, and we label our issues to categorize them. Here are a couple of handy links to check out:
+
+* [Good first issue](https://github.com/kbind-dev/kbind/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) issues
+* [Help wanted](https://github.com/kbind-dev/kbind/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) issues
+
+You're certainly not limited to only these kinds of issues, though! If you're comfortable, please feel free to try working on anything that is open.
+
+We do use the assignee feature in GitHub for issues. If you find an unassigned issue, comment asking if you can be assigned, and ideally wait for a maintainer to respond. If you find an assigned issue and you want to work on it or help out, please reach out to the assignee first.
+
+Sometimes you might get an amazing idea and start working on a huge amount of code. We love and encourage excitement like this, but we do ask that before you embark on a giant pull request, please reach out to the community first for an initial discussion. You could [file an issue](https://github.com/kbind-dev/kbind/issues/new/choose), send a discussion to our [mailing list](https://groups.google.com/g/kube-bind-dev), and/or join one of our [community meetings](https://docs.google.com/document/d/1qztpKOmdZu5iWq_4N9n3AZpcAPuPhBiGNbje5GPg0iM).
+
+Finally, we welcome and value all types of contributions, beyond "just code"! Other types include triaging bugs, tracking down and fixing flaky tests, improving our documentation, helping answer community questions, proposing and reviewing designs, etc.
+
+### Getting your PR Merged
+
+The `kbind` project uses `OWNERS` files to denote the collaborators who can assist you in getting your PR merged. There are two roles: reviewer and approver. Merging a PR requires sign off from both a reviewer and an approver.
+
+## Community Roles
+
+### Reviewers
+
+Reviewers are responsible for reviewing code for correctness and adherence to standards. Oftentimes reviewers will be able to advise on code efficiency and style as it relates to golang or project conventions as well as other considerations that might not be obvious to the contributor.
+
+### Approvers
+
+Approvers are responsible for sign-off on the acceptance of the contribution. In essence, approval indicates that the change is desired and good for the project, aligns with code, API, and system conventions, and appears to follow all required process including adequate testing, documentation, follow ups, or notifications to other areas who might be interested or affected by the change.
+
+Approvers are also reviewers.
+
+### Management of `OWNERS` Files
+
+If a reviewer or approver no longer wishes to be in their current role it is requested that a PR be opened to update the `OWNERS` file. `OWNERS` files may be periodically reviewed and updated based on project activity or feedback to ensure an acceptable contributor experience is maintained.
diff --git a/docs/content/developers/.pages b/docs/content/developers/.pages
new file mode 100644
index 000000000..c4925095a
--- /dev/null
+++ b/docs/content/developers/.pages
@@ -0,0 +1,8 @@
+nav:
+ - index.md
+ - Architecture: architecture.md
+ - Development Environments: dev-environment
+ - Backend: backend
+ - Konnector: konnector
+ - Publishing a release: publishing-a-release.md
+ - Testing changes: testing-changes.md
diff --git a/docs/content/developers/architecture.md b/docs/content/developers/architecture.md
new file mode 100644
index 000000000..8d8d53cee
--- /dev/null
+++ b/docs/content/developers/architecture.md
@@ -0,0 +1,57 @@
+# Architecture Overview
+
+This is development-focused documentation intended to explain resource synchronization between consumer and provider clusters. For user-facing behavior, see the [Synchronization User Guide](../usage/synchronization.md).
+
+v2 separates synchronization from service discovery, authentication, and credential issuance. A core-only deployment runs one consumer-side konnector and no kbind-specific provider controller.
+
+## Core control loop
+
+| Package | Responsibility |
+| --- | --- |
+| `cmd/konnector` | Wire the consumer manager, multicluster manager, provider, and reconcilers |
+| `engine/connection` | Resolve credential Secrets, pin cluster identity, discover APIs, handle schema policies and heartbeat Leases |
+| `engine/provider` | Engage Ready Connections as multicluster-runtime provider clusters, disengage unavailable ones |
+| `engine/binding` | Reconcile ClusterBinding/Binding readiness, selected APIs, related resources, and cleanup |
+| `engine/crdpull` | Install and update consumer CRDs from provider CRDs |
+| `engine/openapi` | Synthesize consumer CRDs from provider discovery and OpenAPI v3 |
+| `engine/sync` | Start per-binding/GVR syncers, reconcile spec/status and ownership, and stop obsolete syncers |
+| `engine/remote` | Read kubeconfigs and determine provider identity |
+| `engine/mapper` | Compile-time object-key mapping interface |
+
+The consumer manager watches core resources. A Ready Connection supplies the credentials for an engaged provider cluster. Syncers use that cluster's client for writes, API reader for fresh reads, and cache watches for provider changes. A periodic resync is a backstop, not the primary status transport.
+
+When a Connection loses readiness, the provider disengages and its syncers stop. Recovery creates a fresh engaged cluster and rebuilds syncers, rather than retaining clients and caches tied to a dead connection. Leader election gates the consumer controllers and provider engagement together.
+
+For Kubernetes, cluster identity uses the `kube-system` namespace UID, on kcp-like providers it uses the `LogicalCluster` UID when available. This is identity pinning, not a replacement for TLS verification or RBAC.
+
+## Service layer
+
+`cmd/backend` wires independently enabled gateway, issuer, and reaper modules. The gateway embeds the static UI from `web/`. The CLI in `cli/` is an HTTP client for that gateway.
+
+```mermaid
+sequenceDiagram
+ participant U as CLI / browser
+ participant G as Gateway
+ participant I as Issuer
+ participant P as Provider API server
+ participant C as Consumer konnector
+ U->>G: Authenticate and select Export
+ G->>P: Create or update Grant
+ I->>P: Provision ServiceAccount, RBAC, token
+ U->>G: Redeem one-time bundle ticket
+ G-->>U: Secret + Connection + ClusterBinding
+ U->>C: Apply bundle to consumer
+ C->>P: Discover, synchronize, heartbeat
+```
+
+Once credentials and the bundle exist, the core does not require the gateway to remain online. Grant revocation still requires the issuer to remove provisioned credentials. The optional reaper observes provider-side heartbeat Leases and removes stale Grants under its configured policy.
+
+## Extension points and boundaries
+
+`backend/auth.Authenticator` provides a gateway identity and optional login routes. `backend/issuer.Issuer` provisions and revokes credentials. The in-tree issuer is Kubernetes-specific, kcp workspace issuance requires a separate implementation.
+
+`engine/mapper.Mapper` translates namespace/name keys for synced API instances. Its two methods must round-trip. Install a custom implementation through `sync.WithMapper(...)` in an out-of-tree build, there is no `mapper` CRD field or flag. The shipped mapper is `Identity`. Mapping does not change resource scope, and related-resource synchronization does not yet use this interface.
+
+The provider's RBAC is the authorization boundary. Conflict markers protect against accidental ownership collisions, they do not prevent a holder of overprivileged credentials from bypassing the konnector.
+
+The historical [slim-core proposal](https://github.com/kbind-dev/kbind/blob/v2-next/docs/proposals/v2-slim-core.md) and [extended-layer proposal](https://github.com/kbind-dev/kbind/blob/v2-next/docs/proposals/v2-extended.md) explain design decisions. They include earlier names and planned behavior, the [user guides](../usage/api-concepts.md) and implementation describe the current contract.
diff --git a/docs/content/developers/backend/.pages b/docs/content/developers/backend/.pages
new file mode 100644
index 000000000..2d2399af9
--- /dev/null
+++ b/docs/content/developers/backend/.pages
@@ -0,0 +1,4 @@
+nav:
+ - index.md
+ - HTTP: http
+ - OIDC: oidc
diff --git a/docs/content/developers/backend/http/.pages b/docs/content/developers/backend/http/.pages
new file mode 100644
index 000000000..99f490834
--- /dev/null
+++ b/docs/content/developers/backend/http/.pages
@@ -0,0 +1,4 @@
+nav:
+ - index.md
+ - cluster-binding.md
+ - api-binding.md
diff --git a/docs/content/developers/backend/http/api-binding.md b/docs/content/developers/backend/http/api-binding.md
new file mode 100644
index 000000000..85b261684
--- /dev/null
+++ b/docs/content/developers/backend/http/api-binding.md
@@ -0,0 +1,25 @@
+# API Binding
+
+v2 binding terminates in ordinary consumer-side Kubernetes objects rather than request/response CRDs.
+
+## Request
+
+An authenticated caller posts an Export name to `POST /api/bind`:
+
+```json
+{"export":"widgets"}
+```
+
+The gateway requires a ready Export, creates or updates the caller's Grant, and waits for the issuer to provision credentials. The response contains `grant`, `pickupURL`, and `expiresAt`.
+
+## Pickup and apply
+
+`GET /api/bundle/{token}` consumes the pickup ticket once and returns a kubeconfig Secret, Connection, and ClusterBinding. The ticket defaults to a five-minute lifetime, the issued credentials remain usable until revoked.
+
+The bundle does not include the konnector or core CRDs. Install those first, then apply the bundle using the CLI, kubectl, or an existing GitOps workflow.
+
+## Revocation
+
+Deleting the provider Grant asks the issuer to remove its credentials. This does not remove the consumer's core objects or automatically delete synced provider instances. For normal teardown, unbind while credentials still work.
+
+See [catalog and issuance](../../../usage/catalog.md) for the lifecycle and the [HTTP reference](index.md) for status codes, authentication, and pickup semantics.
diff --git a/docs/content/developers/backend/http/cluster-binding.md b/docs/content/developers/backend/http/cluster-binding.md
new file mode 100644
index 000000000..071cf209d
--- /dev/null
+++ b/docs/content/developers/backend/http/cluster-binding.md
@@ -0,0 +1,17 @@
+# Cluster Binding
+
+In v2, connecting a consumer has two separate operations: install the konnector, then apply a core bundle that establishes a provider Connection. There is no separate cluster-registration handshake or stored consumer kubeconfig.
+
+## Install the konnector
+
+`GET /api/konnector` returns the core CRDs, namespace, RBAC, ServiceAccount, and Deployment without credentials. It is public and has no server-side write effects. Installing these objects alone does not make the consumer appear in the provider inventory.
+
+## Connect a provider
+
+The [API binding flow](api-binding.md) returns a Secret, Connection, and ClusterBinding. After application, the konnector connects directly to the provider and renews a heartbeat Lease.
+
+`GET /api/clusters` builds the inventory from these Leases and Grants. It is authenticated, but its output is not filtered to the caller's identity.
+
+When explicitly enabled, `POST /api/apply` can install the konnector or apply a bundle using a submitted consumer kubeconfig. This sends consumer credentials through the gateway, local CLI or GitOps apply does not.
+
+See the [HTTP reference](index.md) for request bodies and responses.
diff --git a/docs/content/developers/backend/http/index.md b/docs/content/developers/backend/http/index.md
new file mode 100644
index 000000000..775aa4cc6
--- /dev/null
+++ b/docs/content/developers/backend/http/index.md
@@ -0,0 +1,203 @@
+# HTTP API
+
+This reference describes the implemented `cmd/backend` binary and gateway protocol. See [deployment](../index.md) for Helm, OIDC, HTTPS, and RBAC configuration, and [catalog usage](../../../usage/catalog.md) for the lifecycle.
+
+## Authentication conventions
+
+**Authenticated** in the tables means either:
+
+- A `kbind_session` cookie issued by the gateway's OIDC login flow.
+- `Authorization: Bearer ` containing that same signed, encrypted session token.
+- When `--kubernetes-auth=true`, a provider-cluster bearer token successfully verified through Kubernetes TokenReview.
+
+An arbitrary OIDC ID/access token is not a gateway session token. The Kubernetes authenticator is additive, the binary still configures OIDC when the gateway is enabled. Bearer extraction takes precedence over the session cookie.
+
+Authenticated APIs do not implement per-Export entitlements or caller-filtered inventories. OIDC groups and TokenReview groups are recorded, not used to authorize catalog operations.
+
+### Read and inventory endpoints
+
+| Method | Path | Authentication | Response and semantics |
+| --- | --- | --- | --- |
+| `GET` | `/api/provider` | None | `{name, version?, authMethods, applyEnabled}`. Binary authenticator names are `oidc` and optionally `kubernetes`. |
+| `GET` | `/api/me` | Authenticated | `{subject, displayName?, groups?}` for the caller. |
+| `GET` | `/api/catalog` | Authenticated | `{exports: [...], collections?: [...]}`. Only ready Exports and Collections with visible members are included. |
+| `GET` | `/api/catalog/{export}/instances` | Authenticated | `{export, instances: [...]}`. Current Export's provider-side managed objects and related-resource metadata, best effort per API. Unknown Export: `404`. Unlike catalog browsing, this lookup does not require the Export to be Ready. |
+| `GET` | `/api/clusters` | Authenticated | `{clusters: [...], pending?: [...]}` derived from provider Grants and heartbeat Leases. No consumer kubeconfig is stored or returned. |
+| `GET` | `/api/konnector` | None | Applyable YAML: core CRDs, `kbind` namespace, konnector RBAC/ServiceAccount/Deployment, with the configured konnector image. No credentials and no side effects on GET. |
+| `GET` | `/api/healthz` | None | Plain text `ok`, HTTP `200`, process check only. |
+
+Catalog Export responses contain `name`, `title`, `apis`, optional `description`, `iconURL`, `docs`, and `relatedResources`. Related-resource rows have `resource`, `direction`, and an optional human-readable `selector` summary, not the complete Kubernetes selector structure.
+
+Clusters contain `uid`, `lastHeartbeat`, `live`, and `bindings`, binding rows include `grant`, `export`, identity/subject, boundary namespace, heartbeat, `live`, and `apis` with `name` and `syncedCount`. A cluster is live if any binding's Lease is renewed within twice its lease duration.
+
+Instance rows contain `api`, `namespace` when namespaced, `name`, `createdAt`, and consumer UID/identity when attributable. Related rows additionally have `related: true` and `direction`. The endpoint never returns Secret data. The [inventory guide](../../../usage/catalog.md#cluster-and-instance-inventories) explains attribution and best-effort omissions.
+
+### Bind, pickup, and apply
+
+| Method | Path | Authentication | Important semantics |
+| --- | --- | --- | --- |
+| `POST` | `/api/bind` | Authenticated | JSON body `{"export":"widgets"}`. Create/update the identity's Grant, wait for issuer Ready, then return `{grant, pickupURL, expiresAt}`. `pickupURL` is currently relative to the gateway root. |
+| `GET` | `/api/bundle/{token}` | Possession of token, no session required | Single-use pickup. Default response is YAML containing Secret + Connection + ClusterBinding. `Accept: application/json` returns `{"bundle":[...objects...]}` instead. |
+| `POST` | `/api/apply` | Authenticated, route enabled only by `--enable-apply` | JSON body with base64 consumer `kubeconfig`, optional `export`, and optional `installKonnector`. Apply directly from the gateway. Returns `{"applied":["Kind/name",...]}`. |
+
+Bind behavior:
+
+- Body must contain a nonempty `export`, no namespace, binding-kind, conflict-policy override, or collection-bind parameter exists.
+- Unknown Export: `404`. Non-ready Export or provider-side failure: `502`.
+- Issuer wait timeout: `504`, normally after 30 seconds. There is no command-line flag for this timeout. Retry the bind after checking the issuer and token controller, the deterministic Grant prevents duplicate records for the same identity/Export.
+- Every successful bind replaces any unused pickup token for that Grant. The default pickup expiry is five minutes, not the credential lifetime.
+
+Pickup behavior:
+
+- Unknown, expired, or previously consumed token: `410`.
+- Consumption is recorded atomically on the Grant before bundle assembly. Later assembly/response failure requires a new bind, not a retry of the same URL. A provider read/assembly failure can return `502` after consumption.
+- YAML download uses a `.yaml` attachment filename. JSON contains the same object material, including credentials.
+- The issued kubeconfig uses a long-lived ServiceAccount token. Pickup expiry, browser logout, and session expiry do not revoke it.
+- Treat the URL and response as secrets. Exclude them from shared caches, request logs, analytics, and link scanners.
+
+Apply behavior:
+
+```json
+{
+ "export": "widgets",
+ "kubeconfig": "",
+ "installKonnector": true
+}
+```
+
+`export` may be omitted for konnector installation only. Omitting `export` with `installKonnector: false` is rejected. With an Export, the handler follows the same Grant provisioning path but builds and applies the bundle directly, without consuming a pickup URL. It ensures the `kbind` namespace even when installation is skipped.
+
+Malformed JSON/base64/kubeconfig and “nothing to do” requests return `400`, issuance timeout returns `504`, provider or apply failures generally return `502`. Disabling the feature leaves `/api/apply` unregistered, clients should use `/api/provider.applyEnabled`, not assume a particular disabled-route status.
+
+The gateway must be able to reach the consumer API and use the submitted kubeconfig from its own environment. Base64 is not encryption. Server-side apply forces field ownership, and operations are sequential, not transactional. An error does not imply earlier objects were rolled back.
+
+### Browser and OIDC endpoints
+
+| Method | Path | Authentication | Behavior |
+| --- | --- | --- | --- |
+| `GET` | `/` | Page itself is public | Embedded static app, its authenticated API calls send an unauthenticated browser to `/login`. |
+| `GET` | `/login` | None | Login page with OIDC sign-in. |
+| `GET` | `/api/auth/oidc/login` | None | Start code+PKCE login, optional `redirect` selects final landing location. |
+| `GET` | `/api/auth/oidc/callback` | Valid OIDC flow/state cookie | Exchange code, verify ID token and nonce, issue session, redirect. |
+| `GET` | `/api/auth/logout` | None | Clear the session cookie and redirect to `/login`, not bearer-token or Grant revocation. |
+
+Login `redirect` accepts a local path (default `/`) or an HTTP(S) URL whose host is exactly `localhost`, `127.0.0.1`, or `::1`. A loopback redirect gets the session token appended as the `token` query parameter for the CLI. External hosts and scheme-relative redirects are rejected with `400`.
+
+Login-state cookie `kbind_oidc_state` has a browser Max-Age of ten minutes and path `/api/auth/oidc/`. The session cookie uses the configured session TTL and path `/`. Both are HttpOnly and SameSite=Lax, Secure depends on `r.TLS`, not `--external-url` or forwarded headers. The state token is encoded using the same codec/keys as sessions, the cookie's ten-minute Max-Age should not be confused with a separate server-side flow store.
+
+The app's Catalog and Clusters are in-page views, not additional REST endpoints or browser deep-link routes. There is no browser token-management page, Export editor, Grant revoke endpoint, or `/api/auth/refresh`.
+
+### Error envelopes
+
+Gateway handlers normally return `{"error":"message"}` for errors: authentication failures are `401`, malformed bind/apply requests `400`, upstream Kubernetes failures commonly `502`. OIDC handlers and standard HTTP routing/file-serving errors may be plain text instead. Do not assume every non-2xx response is JSON.
+
+### Calling with a provider-cluster token
+
+For an existing provider ServiceAccount `portal` in namespace `default`, when TokenReview authentication is enabled:
+
+```sh
+TOKEN="$(kubectl --context=provider -n default create token portal)"
+curl -fsS https://bind.example.com/api/catalog \
+ -H "Authorization: Bearer $TOKEN"
+curl -fsS https://bind.example.com/api/bind \
+ -H "Authorization: Bearer $TOKEN" \
+ -H 'Content-Type: application/json' \
+ --data '{"export":"widgets"}'
+unset TOKEN
+```
+
+The response's `pickupURL` can be retrieved without that bearer token. Download once into a protected file, inspect it, and apply using a consumer kubeconfig after core installation. This is also possible with a valid kbind session bearer token, the CLI normally handles issuance and pickup itself.
+
+## Binary flags
+
+Flags below are defined in `cmd/backend/main.go`. Duration values use Go duration syntax such as `30s`, `5m`, or `12h`. See `./bin/backend -help` for library-added logging and Kubernetes client flags as well.
+
+### Modules and CRDs
+
+| Flag | Binary default | Meaning / Helm value |
+| --- | --- | --- |
+| `--enable-gateway` | `true` | HTTP API and UI. `modules.gateway` |
+| `--enable-issuer` | `true` | Grant provisioning and Export validation controllers. `modules.issuer` |
+| `--enable-reaper` | `false` | Lease-based stale Grant sweeper. `modules.reaper` |
+| `--enable-apply` | `false` | Add browser-apply route to gateway. `modules.apply` |
+| `--install-crds` | `true` | Install/refresh service-layer CRDs at process startup, independently of enabled modules. Helm's `installCRDs` controls templates only, pass this flag in `extraArgs` to disable process self-install. |
+
+### Gateway and issued kubeconfigs
+
+| Flag | Binary default | Meaning / Helm value |
+| --- | --- | --- |
+| `--listen-address` | `:8080` | HTTP bind address. Chart constructs `:`, `listenPort: 8080` |
+| `--provider-name` | `kbind` | Human-facing provider name. `providerName` |
+| `--external-url` | Empty | Public gateway base URL used to default OIDC callback. Empty falls back to `http://localhost` plus listen address, intended for a `:PORT` dev address. `externalURL` |
+| `--pickup-ttl` | `5m` | One-time pickup URL lifetime, not credential TTL. `pickupTTL` |
+| `--konnector-image` | `ghcr.io/kbind-dev/konnector:latest` | Image in `/api/konnector` manifests and browser installation. `konnectorImage` |
+| `--external-address` | Empty | Provider API URL embedded in issued kubeconfigs, empty uses backend REST config host. `externalAddress` |
+| `--external-ca-file` | Empty | PEM CA bundle embedded in issued kubeconfigs, empty uses token Secret `ca.crt`. No direct chart value, supply file and `extraArgs`. |
+| `--issuer-scope` | `Cluster` | Granted-resource RBAC in all namespaces (`Cluster`) or boundary-only (`Namespace`). `issuerScope` |
+
+`externalURL` and `externalAddress` are different endpoints. The first is reached by browser/CLI clients, the second by the consumer konnector. Namespace scope does not remap namespaces or change the bundle's `ClusterBinding` kind.
+
+### Sessions
+
+| Flag | Binary default | Meaning / Helm value |
+| --- | --- | --- |
+| `--cookie-signing-key` | Empty | Standard base64 text of HMAC key bytes, use 32/64 bytes. Empty generates a random per-process 64-byte key. `cookieKeys.existingSecret`, key `signingKey` |
+| `--cookie-encryption-key` | Empty | Standard base64 text of AES key bytes (16/24/32). Empty generates a random per-process 32-byte key. Same Secret, key `encryptionKey` |
+| `--session-ttl` | `12h` | Session token and session cookie lifetime. `sessionTTL` |
+
+Both keys must match across replicas. Missing either key breaks that part of cross-replica/restart continuity. The binary has no direct environment-variable configuration for these flags, the chart injects Secret values into environment variables and expands them in container arguments.
+
+### OIDC and Kubernetes authentication
+
+| Flag | Binary default | Meaning / Helm value |
+| --- | --- | --- |
+| `--oidc-issuer-url` | Empty | OIDC discovery issuer. Required for gateway except mock mode. `oidc.issuerURL` |
+| `--oidc-client-id` | Empty | OIDC client ID. Required for gateway except mock mode. `oidc.clientID` |
+| `--oidc-client-secret` | Empty | Confidential client's secret, as required by issuer registration. `oidc.existingSecret`, key `clientSecret` |
+| `--oidc-ca-file` | Empty | PEM CA bundle for OIDC HTTPS, empty uses system roots. A supplied bundle replaces the root pool. No direct chart mount/value, supply file and `extraArgs`. |
+| `--oidc-username-claim` | `sub` | Required nonempty string claim used in stable subject. `oidc.usernameClaim` |
+| `--oidc-groups-claim` | Empty | Optional string or string-array groups claim. Empty means no groups. `oidc.groupsClaim` |
+| `--oidc-scopes` | `openid,profile,email` | Comma-separated requested scopes. `oidc.scopes` |
+| `--oidc-redirect-url` | Empty | Defaults to `/api/auth/oidc/callback`. `oidc.redirectURL` |
+| `--oidc-mock` | `false` | Embedded auto-approving development issuer, replaces normal OIDC config. `oidc.mock` |
+| `--oidc-mock-listen` | Empty | Fixed mock issuer address or random localhost port when empty. Chart uses `127.0.0.1:`, default chart port `5556`. |
+| `--kubernetes-auth` | `false` | Additionally accept provider tokens via TokenReview. `kubernetesAuth` |
+
+Mock mode is not safe for production. There is no public flag to skip TLS verification for a real OIDC issuer, configure trust correctly.
+
+### Reaper
+
+| Flag | Binary default | Meaning / Helm value |
+| --- | --- | --- |
+| `--reaper-ttl` | `30m` | Silence threshold for matching heartbeat Leases, or Ready age with no renewed Lease. `reaper.ttl` |
+| `--reaper-revoke` | `false` | Delete stale Grants, issuer finalizer revokes credentials. `reaper.revoke` |
+| `--reaper-delete-boundary` | `false` | When revoking, also delete boundary namespace if no other non-deleting Grant shares it. `reaper.deleteBoundary` |
+| `--reaper-interval` | `1m` | Sweep cadence. `reaper.interval` |
+
+When `--reaper-revoke=false`, stale marking is non-destructive. Boundary deletion only operates inside revocation, setting it alone does not enable deletion. This policy is separate from the inventory's two-lease-duration Live/Stale display.
+
+### Controller manager
+
+| Flag | Binary default | Meaning / Helm value |
+| --- | --- | --- |
+| `--metrics-bind-address` | `:8086` | Controller-manager metrics, chart constructs `:`. |
+| `--health-probe-bind-address` | `:8082` | Controller-manager health/readiness listener, chart constructs `:`. |
+| `--leader-elect` | `false` | Leader election for issuer/reaper controllers. Chart enables for `leaderElect: true` or `replicaCount > 1`. |
+| `--leader-election-id` | `backend.kbind.io` | Leader-election Lease name. `leaderElectionID` |
+
+The manager exists only when issuer or reaper is enabled. These addresses are not gateway endpoints, gateway-only processes do not start this manager. The binary also registers controller-runtime's `--zap-*` logging flags.
+
+## Helm-specific settings and caveats
+
+The chart is **`deploy/charts/backend-v2`**:
+
+- Image defaults: repository `ghcr.io/kbind-dev/backend`, empty `image.tag` falling back to source `appVersion: "0.0.0"`. Override with an image actually built/published from the intended v2 revision.
+- Gateway Service: `service.type: ClusterIP`, `service.port: 80`, `listenPort: 8080`. No Ingress/TLS or NetworkPolicy templates are included.
+- `replicaCount: 1`, `leaderElect: false`, and no existing cookie key Secret by default. Increasing replicas automatically enables election, **not** key sharing.
+- `rbac.create: true` provisions powerful provider administration permissions. `rbac.syncedResourceGroups: ["*"]` enables broad read inventory, narrow it deliberately. Module flags do not prune RBAC rules.
+- `serviceAccount.create`, `serviceAccount.name`, and `serviceAccount.annotations` configure the runtime identity.
+- `extraArgs` appends binary flags. CA files still need a Deployment mount customization, there are no generic extra-volume values.
+- `installCRDs: false` alone does not disable the process's CRD self-install.
+- The OIDC issuer/client ID template requirements apply even if the gateway module is disabled.
+
+Read [backend setup](../index.md) before exposing a deployment. In particular, HTTPS termination does not make the HTTP backend set Secure cookies, browser apply forwards consumer credentials, and the default issued scope is not namespace-isolated tenancy.
diff --git a/docs/content/developers/backend/index.md b/docs/content/developers/backend/index.md
new file mode 100644
index 000000000..089b85a10
--- /dev/null
+++ b/docs/content/developers/backend/index.md
@@ -0,0 +1,193 @@
+# Backend
+
+The backend adds a catalog, browser login, credential issuance, and provider-side inventory to kbind. It runs **on or against the provider cluster**. The consumer runs the konnector, not the backend.
+
+You do not need a backend to use the core `Connection`, `ClusterBinding`, and `Binding` APIs with credentials supplied by another system. See [installation](../../setup/helm.md) and the [GitOps workflow](../../usage/gitops.md) for that path.
+
+## Modules and placement
+
+One `backend` binary contains independently enabled modules:
+
+| Module | Binary flag / Helm value | Default | Responsibility |
+| --- | --- | --- | --- |
+| Gateway | `--enable-gateway` / `modules.gateway` | `true` | HTTP API, embedded browser UI, login, catalog, bundle pickup, inventories |
+| Issuer | `--enable-issuer` / `modules.issuer` | `true` | Reconcile Grants into Kubernetes credentials, validate Exports |
+| Reaper | `--enable-reaper` / `modules.reaper` | `false` | Mark stale Grants using heartbeat Leases, optionally revoke and delete boundaries |
+| Browser apply | `--enable-apply` / `modules.apply` | `false` | Add `POST /api/apply` to the gateway |
+
+The gateway fronts exactly one provider: the Kubernetes configuration used by the backend process. The binary resolves that configuration through controller-runtime/client-go, using `KUBECONFIG` or an in-cluster ServiceAccount. Changing a user's gateway session does not select a different provider.
+
+Gateway-only deployments need an issuer running elsewhere for new binds to finish. Reaper revocation also needs the issuer to process Grant finalizers. Issuer-only deployments need no OIDC configuration **in the binary**, the current Helm template nevertheless requires `oidc.issuerURL` and `oidc.clientID` unless `oidc.mock` is true, even with `modules.gateway: false`.
+
+The bundled issuer is for plain Kubernetes: it provisions namespaces, ServiceAccounts, RBAC, and Secret-based ServiceAccount tokens. There is no selectable built-in kcp issuer.
+
+## Development: run against a provider
+
+From this v2 source checkout, with a provider kubeconfig and the Go version required by `go.mod`:
+
+```sh
+make backend
+KUBECONFIG=./provider.kubeconfig ./bin/backend \
+ --oidc-mock \
+ --external-url=http://localhost:8080 \
+ --external-address=https://PROVIDER-API-REACHABLE-FROM-CONSUMER
+```
+
+Open `http://localhost:8080` and sign in. The default startup installs or refreshes the `catalog.kbind.io` and `iam.kbind.io` CRDs, so this kubeconfig must permit CRD writes as well as the backend's other operations.
+
+`--oidc-mock` starts a local issuer and **auto-approves every login as the mock user**. It is only for an isolated development environment. It does not simulate production access policy or user separation. The default mock listen port is random, set `--oidc-mock-listen=127.0.0.1:5556` when a stable, port-forwardable issuer URL is needed.
+
+For an in-cluster development deployment, explicitly select a backend image built from this checkout:
+
+```sh
+helm upgrade --install backend ./deploy/charts/backend-v2 \
+ --kube-context=provider \
+ --namespace=kbind-backend --create-namespace \
+ --set fullnameOverride=kbind-backend \
+ --set image.repository=ghcr.io/kbind-dev/backend \
+ --set image.tag=dev \
+ --set oidc.mock=true \
+ --set externalURL=http://localhost:8080 \
+ --set externalAddress=https://PROVIDER-API-REACHABLE-FROM-CONSUMER
+```
+
+The `dev` image must already be available to that cluster, this command does not build, publish, or load it. For example, `make image-backend` builds `ghcr.io/kbind-dev/backend:dev` locally. Load it into your development cluster or push your own tagged image first.
+
+Forward both the gateway and the chart's fixed mock issuer port:
+
+```sh
+kubectl --context=provider -n kbind-backend \
+ port-forward deployment/kbind-backend 8080:8080 5556:5556
+```
+
+The host browser and backend must both be able to reach the issuer URL. A mock login cannot work through a gateway-only port-forward when the issuer port is unreachable.
+
+## Production prerequisites
+
+Before deploying:
+
+1. Install the provider's service CRDs and service controllers. The backend catalogs APIs, it does not implement the services behind them.
+2. Choose a backend image and a konnector image from the intended v2 revision. The source chart has placeholder `version` and `appVersion` **`0.0.0`**, an empty `image.tag` therefore selects `ghcr.io/kbind-dev/backend:0.0.0`. Override it. The installer/CLI default konnector image is **`ghcr.io/kbind-dev/konnector:latest`**, not the chart's version.
+3. Provide an HTTPS gateway URL, a trusted OIDC issuer, and an OIDC client with the callback `https://YOUR-GATEWAY/api/auth/oidc/callback`.
+4. Provide a provider API address reachable from the **consumer konnector**. An in-cluster address such as `https://kubernetes.default.svc` is usually not suitable for a consumer in another cluster.
+5. Review the backend and issued-credential RBAC described below. Authenticated access to this gateway is a significant trust decision.
+
+Use the local chart at `deploy/charts/backend-v2`, not a v0.x chart path or an assumed released v2 chart version. For images you build yourself:
+
+```sh
+make image-backend BACKEND_IMAGE=registry.example.com/platform/backend:v2-example
+make image IMAGE=registry.example.com/platform/konnector:v2-example
+docker push registry.example.com/platform/backend:v2-example
+docker push registry.example.com/platform/konnector:v2-example
+```
+
+Replace the registry and illustrative tags with your own published, revision-pinned images. Configure registry credentials using `imagePullSecrets` when required.
+
+### OIDC and shared session keys
+
+Create the following Secrets in the release namespace. Populate the OIDC client secret using your normal secret-management process, its key must be `clientSecret`. Do not put real secrets in a committed values file.
+
+```sh
+kubectl --context=provider create namespace kbind-backend
+kubectl --context=provider -n kbind-backend create secret generic kbind-oidc \
+ --from-literal=clientSecret="$OIDC_CLIENT_SECRET"
+kubectl --context=provider -n kbind-backend create secret generic kbind-cookie-keys \
+ --from-literal=signingKey="$(openssl rand -base64 64)" \
+ --from-literal=encryptionKey="$(openssl rand -base64 32)"
+```
+
+`signingKey` and `encryptionKey` contain **base64 text** of the random key bytes. Kubernetes Secret storage encodes that text again in `.data`, do not substitute raw bytes for the text expected by the backend flags. Recommended decoded sizes are 32 or 64 bytes for the HMAC signing key, and 16, 24, or 32 bytes for the AES encryption key.
+
+Example `backend-values.yaml`:
+
+```yaml
+fullnameOverride: kbind-backend
+replicaCount: 2
+image:
+ repository: registry.example.com/platform/backend
+ tag: v2-example
+konnectorImage: registry.example.com/platform/konnector:v2-example
+providerName: Example platform
+externalURL: https://bind.example.com
+externalAddress: https://provider-api.example.com:6443
+issuerScope: Cluster
+modules:
+ gateway: true
+ issuer: true
+ reaper: false
+ apply: false
+oidc:
+ issuerURL: https://sso.example.com
+ clientID: kbind
+ existingSecret: kbind-oidc
+ usernameClaim: sub
+ groupsClaim: groups
+ scopes: openid,profile,email,groups
+cookieKeys:
+ existingSecret: kbind-cookie-keys
+sessionTTL: 12h
+pickupTTL: 5m
+rbac:
+ syncedResourceGroups:
+ - example.org
+ - ""
+```
+
+Request the `groups` scope only if your identity provider supports it. The empty API group above enables inventory reads of ConfigMaps as well as the example custom API, adjust the groups to what your catalog uses.
+
+```sh
+helm upgrade --install backend ./deploy/charts/backend-v2 \
+ --kube-context=provider \
+ --namespace=kbind-backend \
+ --values=backend-values.yaml
+kubectl --context=provider -n kbind-backend rollout status deployment/kbind-backend
+```
+
+The chart exposes a ClusterIP Service on port 80, targeting HTTP port 8080. It does **not** create an Ingress, Gateway API route, TLS certificate, or NetworkPolicy. Supply those through your platform.
+
+### HTTPS and cookies
+
+The binary serves HTTP with `ListenAndServe`, it has no TLS-certificate flags. Terminate HTTPS at a trusted proxy and restrict direct access to the backend. Use HTTPS for production gateway, OIDC, and provider API endpoints.
+
+Important current behavior:
+
+- Both `kbind_session` and `kbind_oidc_state` are `HttpOnly`, `SameSite=Lax` cookies. The session cookie uses `/`, the login-state cookie uses `/api/auth/oidc/`.
+- The cookie `Secure` attribute is set only when the backend request has `r.TLS != nil`. With this binary's HTTP listener behind TLS termination it is **not automatically set**, even when `externalURL` is HTTPS. `X-Forwarded-Proto` does not change that behavior. Configure the HTTPS proxy to add `Secure` to both cookies, and verify the browser's actual `Set-Cookie` responses.
+- The encrypted, signed session token is accepted either as the cookie or as `Authorization: Bearer `. This is a kbind session token, not an arbitrary OIDC ID token or access token.
+- Sessions default to 12 hours. There is no refresh endpoint or per-session server-side revocation store. Browser logout clears the cookie, an already copied bearer token remains usable until expiry or key replacement.
+- Missing keys produce random per-process keys. Supply **both** shared keys for restart-stable sessions and multiple gateway replicas. Replacing the keys invalidates existing sessions and in-progress login state.
+
+OIDC uses authorization code flow with PKCE, state, and nonce validation. `oidc.usernameClaim` defaults to `sub`, the resulting identity subject is `#`. Changing the issuer or username claim can create new identities, Grants, and boundary namespaces. Groups are recorded as identity metadata, they are not an Export authorization policy.
+
+For a private OIDC CA, the binary supports `--oidc-ca-file`. For a provider CA that differs from the ServiceAccount token Secret's `ca.crt`, use `--external-ca-file`. These are distinct trust paths. The chart has no direct CA-file or extra-volume values: make the files available with an appropriate Deployment customization and pass the flags through `extraArgs`. Merely passing a local filename in Helm values does not mount that file into the container.
+
+### Kubernetes TokenReview authentication
+
+Set `kubernetesAuth: true` (binary: `--kubernetes-auth`) to additionally accept provider-cluster bearer tokens. The backend submits a TokenReview to its provider, the chart includes `create` on `authentication.k8s.io/tokenreviews`. The identity subject is `kubernetes#`.
+
+This is for API integrations that already hold a provider identity. It is not a second interactive login method in the browser or CLI, and does not replace OIDC configuration in the gateway binary. `kubectl bind login` still uses OIDC. See the [HTTP reference](http/index.md) for request examples.
+
+The gateway authenticates callers but does not perform per-Export entitlement checks or caller-scoped inventory filtering. Every authenticated caller can browse the ready catalog, bind its Exports, and read the provider's cluster and instance inventories. TokenReview verifies identity, not authorization to those operations. Do not expose this as a tenant-isolated marketplace without an additional access-control design.
+
+## Credential scope and installer privileges
+
+`issuerScope: Cluster` is the default. Each Grant gets permissions on its named APIs across provider namespaces, including their `/status` resources, plus namespace creation. Related-resource rules add Secret/ConfigMap access: read-only for `FromProvider`, read/write for `FromConsumer`.
+
+`issuerScope: Namespace` puts those resource permissions only in the identity's boundary namespace. It does not rename namespaces, change the generated `ClusterBinding` into a `Binding`, or authorize cluster-scoped resources. Identity-preserving sync requires compatible consumer namespace names and provider list/watch permissions, do not treat this setting as a transparent drop-in isolation switch for the default cluster-wide bundle.
+
+Both scopes include cluster-wide CRD reads, a read of `kube-system` for provider identity, and heartbeat Lease permissions inside the boundary. Related-resource selectors constrain konnector sync, **not** Kubernetes RBAC: the issued role does not restrict reads to the selected labels or names.
+
+The backend's own chart RBAC includes namespace/credential management, `escalate` and `bind` on Roles/ClusterRoles, and wildcard inventory reads by default. Narrow `rbac.syncedResourceGroups`, and use reviewed custom RBAC with `rbac.create: false` if necessary. Disabling a module does not automatically remove its chart RBAC.
+
+Keep browser apply off unless intentionally granting the gateway access to consumer API endpoints and credentials. The browser sends an entire base64-encoded kubeconfig, base64 is not encryption. Use a trusted, self-contained kubeconfig, avoid local-file and exec-plugin dependencies, restrict gateway egress, and exclude credentials/request bodies from logs. CLI apply keeps the consumer kubeconfig local, it does not use `/api/apply`.
+
+All built-in install/apply paths use server-side apply with **forced field ownership**. They can overwrite drift in existing kbind installation resources. Review the embedded konnector's broad cluster RBAC, pin its image, and do not mix automatic install/upgrade with a separately managed Helm installation without understanding the ownership consequences.
+
+## CRDs, replicas, and operations
+
+- `installCRDs` in Helm controls chart-rendered service-layer CRDs only. `--install-crds` independently defaults to `true` in the process. To manage CRDs out of band, install them first, set `installCRDs: false`, and add `--install-crds=false` to `extraArgs`.
+- With `replicaCount > 1`, the chart enables leader election automatically. Gateway replicas serve requests independently, issuer/reaper controllers run under leader election. Set shared session keys yourself: the chart does not enforce their presence.
+- Pickup state lives in Grant annotations, not gateway memory. It works across replicas. Back up provider Kubernetes state according to your credential and recovery policy, sessions also depend on the key Secret.
+- `/api/healthz` is a process HTTP check, not proof of functioning OIDC, catalog validation, or credential issuance. Check an actual login, a ready Export, and a bind before declaring the deployment usable.
+
+Enable the reaper conservatively with `modules.reaper: true` and `reaper.revoke: false`. Review its `Stale` conditions before enabling revocation. The [catalog guide](../../usage/catalog.md) describes lifecycle and destructive cleanup. Consult the [flag and endpoint reference](http/index.md) and [troubleshooting](../../usage/troubleshooting.md) for operational detail.
diff --git a/docs/content/developers/backend/oidc/index.md b/docs/content/developers/backend/oidc/index.md
new file mode 100644
index 000000000..c654f5410
--- /dev/null
+++ b/docs/content/developers/backend/oidc/index.md
@@ -0,0 +1,94 @@
+---
+title: OIDC Authentication
+weight: 30
+---
+
+# OIDC Authentication
+
+The kbind backend supports OpenID Connect (OIDC) authentication for securing API access. There are two modes of operation: external OIDC providers and an embedded OIDC provider for development.
+
+## External OIDC Provider (Production)
+
+For production deployments, use an external OIDC provider such as:
+
+- Dex
+- Keycloak
+- Auth0
+- Google
+- Microsoft Azure AD
+- Any OIDC-compliant provider
+
+### Configuration
+
+Configure the backend using the v2 flags:
+
+```bash
+./bin/backend \
+ --external-url=https://bind.example.com \
+ --oidc-issuer-url=https://your-oidc-provider.com \
+ --oidc-client-id=your-client-id \
+ --oidc-client-secret="$OIDC_CLIENT_SECRET" \
+ --oidc-redirect-url=https://bind.example.com/api/auth/oidc/callback
+```
+
+Register the exact callback with your issuer. Use `--oidc-ca-file` for a private CA, `--oidc-username-claim` for the stable identity claim (default `sub`), and `--oidc-groups-claim` if groups should be recorded.
+
+### External OIDC Flow
+
+1. User initiates authentication by accessing the gateway login page.
+2. Backend redirects to the OIDC authorization endpoint with state, nonce, and PKCE.
+3. User authenticates with the OIDC provider.
+4. Provider redirects to the backend callback with an authorization code.
+5. Backend exchanges the code, verifies the ID token, and issues a signed, encrypted session.
+6. User accesses protected APIs using the session cookie or token.
+
+## Embedded OIDC Provider (Development Only)
+
+For development and testing, kbind includes an embedded provider that avoids setting up an external authentication service:
+
+```bash
+./bin/backend --oidc-mock --external-url=http://localhost:8080
+```
+
+### Embedded OIDC Flow
+
+1. Backend initializes the mock OIDC issuer and its client credentials.
+2. User starts login through the gateway.
+3. The mock issuer auto-approves the fixed development identity.
+4. Backend completes the same callback and session flow.
+
+The mock issuer uses a separate port. With port forwarding, expose that port as well as the gateway, see [Backend setup](../index.md#development-run-against-a-provider).
+
+## Security Considerations
+
+### External OIDC (Production)
+
+- Use HTTPS for all endpoints and validate provider certificates.
+- Use strong client secrets and exact redirect URI restrictions.
+- Share both signing and encryption keys across gateway replicas.
+- Configure the TLS proxy to add `Secure` to session and login-state cookies: the HTTP backend does not infer it from `externalURL` or forwarded headers.
+- Monitor for security updates to the OIDC provider.
+
+### Embedded OIDC (Development Only)
+
+- **Never use in production**: every login is auto-approved.
+- It is intended only for isolated local development and testing.
+- It does not model distinct users or production authorization.
+
+## Access Control
+
+v2 does not have `--oidc-allowed-groups` or `--oidc-allowed-users`. OIDC groups are identity metadata, not per-Export authorization. All authenticated gateway callers can bind ready Exports and read the provider's inventories. Control access to the gateway accordingly, do not assume the old group-based binding policy is still enforced.
+
+Provider-cluster bearer tokens can additionally be authenticated using `--kubernetes-auth` and TokenReview. This does not replace gateway OIDC login configuration. See [authentication conventions](../http/index.md#authentication-conventions).
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Certificate validation failures**: configure `--oidc-ca-file` with the issuer's trusted CA.
+2. **Callback URL mismatches**: register `/api/auth/oidc/callback` at the public gateway URL.
+3. **Token validation errors**: confirm the issuer URL and configured username claim.
+4. **Network connectivity**: verify both browser and backend can reach the issuer.
+5. **Sessions lost across replicas/restarts**: configure both shared session keys.
+
+See the [backend reference](../http/index.md) for complete flags and cookie lifetimes.
diff --git a/docs/content/developers/dev-environment/.pages b/docs/content/developers/dev-environment/.pages
new file mode 100644
index 000000000..3a8c57d91
--- /dev/null
+++ b/docs/content/developers/dev-environment/.pages
@@ -0,0 +1,4 @@
+nav:
+ - index.md
+ - kcp.md
+ - kind.md
diff --git a/docs/content/developers/dev-environment/index.md b/docs/content/developers/dev-environment/index.md
new file mode 100644
index 000000000..b04344d4b
--- /dev/null
+++ b/docs/content/developers/dev-environment/index.md
@@ -0,0 +1,25 @@
+---
+description: >
+ How to setup a development environment for contributing to kube-bind.
+title: Development Environments
+---
+
+# Development Environments
+
+Due to the fact that kube-bind is by nature a multi-cluster system, for development purposes it's recommended to have multiple clusters running or use kcp to simulate multiple clusters. Below are instructions for both approaches.
+
+All the instructions assume you have already cloned the kube-bind repository and have Go installed.
+
+* You can use [kcp](kcp.md) for a lightweight backend system.
+* You can also use [kind](kind.md) for a more full-featured local Kubernetes cluster.
+
+For v2, check out `v2-next` and use the Go version in `go.mod`:
+
+```bash
+git clone --branch v2-next https://github.com/kbind-dev/kbind.git
+cd kbind
+make build
+make konnector backend bind
+```
+
+The root module references `sdk/` locally, no workspace file is required. See [Testing changes](../testing-changes.md) for the unit and envtest suites.
diff --git a/docs/content/developers/dev-environment/kcp.md b/docs/content/developers/dev-environment/kcp.md
new file mode 100644
index 000000000..c1674a143
--- /dev/null
+++ b/docs/content/developers/dev-environment/kcp.md
@@ -0,0 +1,74 @@
+---
+description: >
+ How to use a kcp provider with the v2 core.
+title: kcp
+---
+
+# Development Environment using kcp
+
+All the instructions assume you have already cloned the kbind repository and have Go installed.
+
+kcp requires initial setup before it can be used. This includes setting up the provider workspace and its `APIResourceSchemas` and `APIExports`. This is not required if you control the setup with your own scripts.
+
+It's useful to have the kcp CLI installed for workspace management:
+
+```bash
+kubectl krew index add kcp-dev https://github.com/kcp-dev/krew-index.git
+kubectl krew install kcp-dev/kcp
+kubectl krew install kcp-dev/ws
+kubectl krew install kcp-dev/create-workspace
+```
+
+## Preparation
+
+Start kcp and populate the provider workspace using the [kcp documentation](https://docs.kcp.io/). v2 does not include the old `make run-kcp` or `kcp-init` bootstrap helpers.
+
+The workspace should expose only the service APIs intended for consumers. Give the provider credential discovery/OpenAPI access, the required instance permissions, and read access to `logicalclusters.core.kcp.io/cluster`.
+
+## Provider
+
+### Kubeconfig Setup
+
+Use a self-contained kubeconfig pointing at the selected logical cluster. Confirm the workspace identity:
+
+```bash
+kubectl --kubeconfig=provider.kubeconfig get logicalcluster cluster
+```
+
+The konnector pins that LogicalCluster UID. On a CRD-less provider, choose `schema.source: OpenAPI` explicitly, the current `Auto` path does not fall back when CRD listing returns an error.
+
+## Consumer
+
+### Initialization
+
+Install the konnector and its three core CRDs on a Kubernetes consumer using the [installation guide](../../setup/helm.md). Deliver the provider kubeconfig as a Secret in the consumer's `kbind` namespace.
+
+### Binding
+
+```yaml
+apiVersion: core.kbind.io/v1alpha1
+kind: Connection
+metadata:
+ name: workspace-provider
+spec:
+ kubeconfigSecretRef:
+ namespace: kbind
+ name: workspace-provider
+ key: kubeconfig
+ schema:
+ source: OpenAPI
+ pullPolicy: Bound
+ updatePolicy: Always
+```
+
+Add a Binding or ClusterBinding selecting the actual `.` names in that workspace. See [API concepts](../../usage/api-concepts.md) for the manifest and [schema limits](../../usage/api-concepts.md#schema-updates-and-fidelity) before relying on synthesized validation or multi-version behavior.
+
+### Launch Konnector
+
+For a host-run development process rather than a Pod:
+
+```bash
+KUBECONFIG=consumer.kubeconfig go run ./cmd/konnector
+```
+
+The core preserves namespace, name, and scope. It does not provision workspaces or translate cluster-scoped APIs into namespaced APIs. The optional backend's in-tree issuer is Kubernetes-specific, its former multicluster-provider flags are not part of the v2 backend.
diff --git a/docs/content/developers/dev-environment/kind.md b/docs/content/developers/dev-environment/kind.md
new file mode 100644
index 000000000..67933d296
--- /dev/null
+++ b/docs/content/developers/dev-environment/kind.md
@@ -0,0 +1,118 @@
+---
+description: >
+ Set up a local provider and consumer for kbind development.
+title: kind
+---
+
+# Development Environment using kind
+
+This guide sets up a provider backend, a consumer konnector, and a Widget catalog offering for the [Quickstart](../../setup/quickstart.md). Both processes run on your workstation against two kind clusters.
+
+## Pre-requisites
+
+- [Docker](https://docs.docker.com/get-docker/), [kind](https://kind.sigs.k8s.io/), and [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl).
+- A v2 source checkout and Go matching `go.mod`.
+- The [kubectl bind plugin](../../setup/kubectl-plugin.md) installed.
+
+Use disposable clusters. The kubeconfigs below contain administrator credentials, keep them out of version control and remove them after the demo. Run commands from the repository root.
+
+## Setup
+
+Create the two clusters and save their kubeconfigs:
+
+```bash
+kind create cluster --name provider
+kind create cluster --name consumer
+
+kind get kubeconfig --name provider > provider.kubeconfig
+kind get kubeconfig --name consumer > consumer.kubeconfig
+make backend konnector
+```
+
+These kubeconfigs use workstation endpoints. They work for the host-run processes below, not for konnector Pods in another cluster.
+
+## Provider
+
+In a second terminal, start the backend:
+
+```bash
+KUBECONFIG="$PWD/provider.kubeconfig" ./bin/backend \
+ --oidc-mock --external-url=http://localhost:8080
+```
+
+Leave it running. Mock OIDC automatically approves logins and is for this local development environment only. The backend installs its catalog and IAM CRDs.
+
+Back in the first terminal, wait for the catalog APIs and seed the example:
+
+```bash
+kubectl --kubeconfig=provider.kubeconfig \
+ wait --for=condition=Established --timeout=120s \
+ crd/exports.catalog.kbind.io crd/collections.catalog.kbind.io
+kubectl --kubeconfig=provider.kubeconfig \
+ apply -f hack/tilt/seed/widgets.yaml
+kubectl --kubeconfig=provider.kubeconfig \
+ wait --for=condition=Ready exports.catalog.kbind.io/widgets --timeout=120s
+```
+
+The manifest installs the Widget CRD, related Secret and ConfigMap, an Export, and a Collection. It does not include a Widget controller.
+
+## Consumer
+
+Install the core CRDs:
+
+```bash
+kubectl --kubeconfig=consumer.kubeconfig apply \
+ -f sdk/config/crd/core.kbind.io_connections.yaml \
+ -f sdk/config/crd/core.kbind.io_clusterbindings.yaml \
+ -f sdk/config/crd/core.kbind.io_bindings.yaml
+kubectl --kubeconfig=consumer.kubeconfig \
+ wait --for=condition=Established --timeout=120s \
+ crd/connections.core.kbind.io crd/clusterbindings.core.kbind.io \
+ crd/bindings.core.kbind.io
+```
+
+In a third terminal, start the konnector:
+
+```bash
+KUBECONFIG="$PWD/consumer.kubeconfig" ./bin/konnector
+```
+
+Leave it running. Do not also install a konnector Deployment on this consumer.
+
+## Login and bind a service
+
+The environment is ready:
+
+| Setting | Value |
+| --- | --- |
+| Provider kubeconfig | `provider.kubeconfig` |
+| Consumer kubeconfig | `consumer.kubeconfig` |
+| Backend URL | `http://localhost:8080` |
+| Catalog Export | `widgets` |
+
+Return to the [Quickstart](../../setup/quickstart.md#quick-development-setup) to log in, bind the Widget API, and create an instance.
+
+The example Secret and ConfigMap are copied from the provider after binding. For instance status updates, simulate the service controller:
+
+```bash
+kubectl --kubeconfig=provider.kubeconfig -n default \
+ patch widget my-widget --subresource=status --type=merge \
+ -p '{"status":{"phase":"Running"}}'
+kubectl --kubeconfig=consumer.kubeconfig -n default \
+ wait --for=jsonpath='{.status.phase}'=Running widget/my-widget --timeout=120s
+```
+
+See [Synchronization](../../usage/synchronization.md) for spec, status, related resources, and unbinding behavior.
+
+## Cleanup
+
+Stop both host processes with Ctrl-C. Remove only the disposable clusters and credentials created for this walkthrough:
+
+```bash
+kind delete cluster --name consumer
+kind delete cluster --name provider
+rm provider.kubeconfig consumer.kubeconfig
+unset KUBECONFIG
+```
+
+For shared clusters, [unbind](../../usage/synchronization.md#unbinding-and-deletion) while the konnector and credentials still work instead of deleting the clusters.
diff --git a/docs/content/developers/index.md b/docs/content/developers/index.md
new file mode 100644
index 000000000..96b46798f
--- /dev/null
+++ b/docs/content/developers/index.md
@@ -0,0 +1,12 @@
+# Developers
+
+Welcome to the kube-bind developer documentation! This section contains information for developers who want to contribute to the kube-bind project, including guides on setting up a development environment, understanding the architecture, and contributing code, and API documentation.
+
+## Developer Guide
+
+- [Architecture](architecture.md)
+- [Development environments](dev-environment/index.md)
+- [Backend](backend/index.md)
+- [Konnector](konnector/index.md)
+- [Publishing a release](publishing-a-release.md)
+- [Testing changes](testing-changes.md)
diff --git a/docs/content/developers/konnector/.pages b/docs/content/developers/konnector/.pages
new file mode 100644
index 000000000..b6df526d4
--- /dev/null
+++ b/docs/content/developers/konnector/.pages
@@ -0,0 +1,3 @@
+nav:
+ - index.md
+ - Controllers: controllers
diff --git a/docs/content/developers/konnector/controllers/.pages b/docs/content/developers/konnector/controllers/.pages
new file mode 100644
index 000000000..254fd00a0
--- /dev/null
+++ b/docs/content/developers/konnector/controllers/.pages
@@ -0,0 +1,5 @@
+nav:
+ - konnector.md
+ - connection.md
+ - binding.md
+ - synchronization.md
diff --git a/docs/content/developers/konnector/controllers/binding.md b/docs/content/developers/konnector/controllers/binding.md
new file mode 100644
index 000000000..e08708410
--- /dev/null
+++ b/docs/content/developers/konnector/controllers/binding.md
@@ -0,0 +1,97 @@
+# Binding Controllers
+
+The ClusterBinding and Binding controllers watch their respective binding objects and referenced `Connections` in the **consumer cluster**. Both use the reconciliation logic in `engine/binding`.
+
+They are responsible for:
+
+* Resolving a Ready Connection and validating the requested exported APIs.
+* Installing or reading consumer schemas according to the Connection's policies.
+* Synchronizing selected related Secrets and ConfigMaps.
+* Reporting bound APIs, schema hashes, conflict counts, and readiness.
+* Cleaning up instances and related-resource copies when a binding is deleted.
+
+A ClusterBinding operates cluster-wide. A Binding limits instance selection and related-resource processing to its namespace, but does not change CRD scope or make the dynamic instance informers namespace-scoped.
+
+## Overview
+
+The chart shows normal reconciliation after the cleanup finalizer has been added. Binding and Connection events trigger reconciliation, with a 30-second periodic refresh for related resources and conflict counts.
+
+```mermaid
+flowchart TD
+ start(["Binding or Connection event or periodic reconciliation"])
+ get_binding(["Get binding"])
+ exists{"Binding exists?"}
+ connection{"Referenced Connection exists and is Ready?"}
+ pending(["Set Ready=False with Pending"])
+ provider(["Build provider client from Connection credentials"])
+ schemas(["For each requested API Check export and process its schema"])
+ counts(["Record boundAPIs and schema hashes Count instance conflict annotations"])
+ missing{"Any APIs not exported?"}
+ not_exported(["Set Ready=False with APINotExported"])
+ waiting{"Waiting for an OpenAPI-source consumer CRD?"}
+ related(["Sync selected related Secrets and ConfigMaps Remove copies that no longer match"])
+ conditions(["Update Conflicts Set Synced=True and Ready=True"])
+ status(["Persist changed status Requeue after 30 seconds"])
+ stop(["Stop"])
+
+ start --> get_binding
+ get_binding --> exists
+ exists -->|no| stop
+ exists -->|yes| connection
+ connection -->|no| pending
+ connection -->|yes| provider
+ provider --> schemas
+ schemas --> counts
+ counts --> missing
+ missing -->|yes| not_exported
+ missing -->|no| waiting
+ waiting -->|yes| pending
+ waiting -->|no| related
+ related --> conditions
+ pending --> status
+ not_exported --> status
+ conditions --> status
+ status --> stop
+```
+
+For `CRD`, reconciliation pulls each exported provider schema with creation and updates controlled by `pullPolicy` and `updatePolicy`. For `OpenAPI`, it synthesizes a missing consumer CRD unless `pullPolicy: None`. An existing OpenAPI-source CRD is read, not refreshed by this controller.
+
+A forbidden CRD pull sets `PermissionDenied=True` and `Ready=False`. Other API errors are returned for retry. Conflicts can coexist with `Ready=True`, and `Synced=True` describes setup, not proof that every instance has reached the provider. Instance synchronization runs separately.
+
+## Deletion
+
+```mermaid
+flowchart TD
+ start(["Deleting binding with cleanup finalizer"])
+ provider(["Try to obtain provider credentials"])
+ next_api{"More APIs to clean up?"}
+ covered{"Another non-deleting binding lists this API?"}
+ schema{"Consumer CRD provides a usable API version?"}
+ drain(["Drain instances in scope Release their syncer finalizers"])
+ cluster_binding{"ClusterBinding?"}
+ remove_crd(["Delete consumer CRD"])
+ related(["Delete related-resource copies owned by this binding"])
+ release(["Remove binding cleanup finalizer"])
+ stop(["Stop"])
+
+ start --> provider
+ provider --> next_api
+ next_api -->|yes| covered
+ covered -->|yes| next_api
+ covered -->|no| schema
+ schema -->|no| next_api
+ schema -->|yes| drain
+ drain --> cluster_binding
+ cluster_binding -->|yes| remove_crd
+ cluster_binding -->|no| next_api
+ remove_crd --> next_api
+ next_api -->|no| related
+ related --> release
+ release --> stop
+```
+
+Draining requests deletion only for owned provider copies, unless an instance uses `deletion-policy: Orphan`. Provider-client and provider-read failures can leave copies behind, but actual provider-delete errors can block cleanup. Binding cleanup does not wait for provider finalizers as normal instance deletion does.
+
+The overlap check is API-wide, not a comparison of namespaces or Connections. A last ClusterBinding can delete the consumer CRD even when it was externally installed. A namespaced Binding keeps the shared CRD. Read the [cleanup limits](../../../usage/synchronization.md#delete-a-binding) before changing overlapping bindings or relying on automatic pruning.
+
+Implementation: [binding reconcilers](https://github.com/kbind-dev/kbind/blob/v2-next/engine/binding/reconciler.go), [related resources](https://github.com/kbind-dev/kbind/blob/v2-next/engine/binding/related.go), and [cleanup](https://github.com/kbind-dev/kbind/blob/v2-next/engine/binding/cleanup.go).
diff --git a/docs/content/developers/konnector/controllers/connection.md b/docs/content/developers/konnector/controllers/connection.md
new file mode 100644
index 000000000..71c101cd3
--- /dev/null
+++ b/docs/content/developers/konnector/controllers/connection.md
@@ -0,0 +1,105 @@
+# Connection Controller
+
+The Connection controller watches `Connections` and their referenced `Secrets` in the **consumer cluster**. It is implemented in `engine/connection`.
+
+It is responsible for:
+
+* Validating the provider kubeconfig and pinning provider/consumer identities.
+* Discovering exported APIs and recording the active schema source.
+* Installing schemas eagerly when `pullPolicy: All`.
+* Maintaining an automatic ClusterBinding when `autoBind` is enabled.
+* Renewing the provider heartbeat Lease and protecting credentials during cleanup.
+
+## Overview
+
+The chart below shows a non-deleting Connection. Successful reconciliation requeues after 30 seconds by default, so newly exported APIs are discovered without requiring a Secret or Connection edit.
+
+```mermaid
+flowchart TD
+ start(["Connection or Secret event or periodic reconciliation"])
+ get_connection(["Get Connection"])
+ exists{"Connection exists?"}
+ finalizers(["Ensure cleanup finalizers on Connection and available Secret"])
+ added{"Connection finalizer just added?"}
+ credentials{"Provider kubeconfig usable?"}
+ invalid(["Set SecretValid=False and Ready=False"])
+ identity(["Set SecretValid=True Read and verify cluster identities"])
+ connected{"Provider identity readable and matches pinned UID?"}
+ unavailable(["Record identity failure Set Ready=False"])
+ pin(["Record cluster UIDs Set Connected=True"])
+ discover(["Discover exported APIs Record activeSchemaSource and exportedAPIs"])
+ pull_all{"pullPolicy is All?"}
+ install(["Pull or synthesize all exported CRDs Honor updatePolicy"])
+ auto_bind{"autoBind enabled?"}
+ binding(["Update managed ClusterBinding Delete it if no APIs are exported"])
+ heartbeat(["Attempt to renew provider Lease"])
+ ready(["Set Ready=True"])
+ status(["Persist changed status Requeue after 30 seconds"])
+ stop(["Stop"])
+
+ start --> get_connection
+ get_connection --> exists
+ exists -->|no| stop
+ exists -->|yes| finalizers
+ finalizers --> added
+ added -->|yes| stop
+ added -->|no| credentials
+ credentials -->|no| invalid
+ invalid --> status
+ credentials -->|yes| identity
+ identity --> connected
+ connected -->|no| unavailable
+ unavailable --> status
+ connected -->|yes| pin
+ pin --> discover
+ discover --> pull_all
+ pull_all -->|yes| install
+ pull_all -->|no| auto_bind
+ install --> auto_bind
+ auto_bind -->|yes| binding
+ auto_bind -->|no| heartbeat
+ binding --> heartbeat
+ heartbeat --> ready
+ ready --> status
+ status --> stop
+```
+
+Provider identity/discovery RBAC denials set `PermissionDenied=True` and `Ready=False`. Other API errors are returned for retry rather than continuing through the success path. Heartbeat errors are logged but do not prevent Ready.
+
+`CRD` discovery uses the export label. `OpenAPI` uses discovery and OpenAPI v3. `Auto` tries CRDs first and switches to OpenAPI only after a successful list with no exports, not after a list error. `Bound` defers installation to binding reconciliation, and `None` leaves creation to an external manager.
+
+## Deletion
+
+```mermaid
+flowchart TD
+ start(["Deleting Connection"])
+ finalizer{"Cleanup finalizer present?"}
+ auto_bind{"autoBind enabled?"}
+ remove_binding(["Request deletion of the managed ClusterBinding"])
+ references{"Any bindings still reference this Connection?"}
+ wait_bindings(["Set Ready=False with DrainingBindings Requeue after 2 seconds"])
+ shared_secret{"Another non-deleting Connection uses the same Secret?"}
+ release_secret(["Remove Secret cleanup finalizer if the Secret still exists"])
+ release_connection(["Remove Connection cleanup finalizer"])
+ stop(["Stop"])
+
+ start --> finalizer
+ finalizer -->|no| stop
+ finalizer -->|yes| auto_bind
+ auto_bind -->|yes| remove_binding
+ auto_bind -->|no| references
+ remove_binding --> references
+ references -->|yes| wait_bindings
+ wait_bindings --> stop
+ references -->|no| shared_secret
+ shared_secret -->|yes| release_connection
+ shared_secret -->|no| release_secret
+ release_secret --> release_connection
+ release_connection --> stop
+```
+
+Explicit bindings are not deleted by this controller. They must be removed separately while credentials and the konnector are still available. Releasing the Secret finalizer does not itself delete the Secret.
+
+See [API concepts](../../../usage/api-concepts.md) for schema policies and credential-rotation limits, and [lifecycle](../../../usage/synchronization.md) for heartbeat and deletion behavior.
+
+Implementation: [Connection reconciler](https://github.com/kbind-dev/kbind/blob/v2-next/engine/connection/reconciler.go).
diff --git a/docs/content/developers/konnector/controllers/konnector.md b/docs/content/developers/konnector/controllers/konnector.md
new file mode 100644
index 000000000..a3581ca7a
--- /dev/null
+++ b/docs/content/developers/konnector/controllers/konnector.md
@@ -0,0 +1,72 @@
+# Konnector
+
+The konnector coordinates the consumer controllers and a provider-cluster connection for each Ready `Connection`. The `connection-provider` controller in `engine/provider` watches `Connections` in the **consumer cluster**.
+
+It is responsible for:
+
+* Starting a provider client and cache when a Connection becomes Ready.
+* Making that provider available to the multicluster manager.
+* Stopping the provider cache when its Connection disappears or loses readiness.
+
+The [Connection controller](connection.md) validates credentials and discovers APIs. The [Binding controllers](binding.md) prepare consumer schemas, and the [Synchronization controllers](synchronization.md) start or stop instance syncers. These replace the old `APIServiceBinding` and per-provider controller arrangement.
+
+## Overview
+
+```mermaid
+flowchart TD
+ start(["Connection event"])
+ get_connection(["Get Connection"])
+ connection_exists{"Connection exists?"}
+ manager_ready{"Multicluster manager available?"}
+ connection_ready{"Connection Ready?"}
+ already_engaged{"Provider already engaged?"}
+ get_credentials(["Read referenced Secret and build provider configuration"])
+ start_cache(["Create provider client and cache Start cache and wait for sync"])
+ engage(["Engage provider with the multicluster manager"])
+ disengage(["Cancel provider cache Remove engaged provider"])
+ retry(["Requeue after 2 seconds"])
+ stop(["Stop"])
+
+ start --> get_connection
+ get_connection --> connection_exists
+ connection_exists -->|no| disengage
+ connection_exists -->|yes| manager_ready
+ manager_ready -->|no| retry
+ manager_ready -->|yes| connection_ready
+ connection_ready -->|no| disengage
+ connection_ready -->|yes| already_engaged
+ already_engaged -->|yes| stop
+ already_engaged -->|no| get_credentials
+ get_credentials --> start_cache
+ start_cache --> engage
+ engage --> stop
+ disengage --> stop
+ retry --> stop
+```
+
+Configuration, cache-start, and engagement errors are returned for retry. A failed engagement cancels the new cache and removes its entry. Connection events also enqueue the managed-CRD controller, which stops instance syncers when readiness is lost. Recovery creates a fresh provider client/cache, and syncers are rebuilt against that new provider.
+
+An already-engaged Ready Connection is left unchanged. Updating a valid credential Secret alone does not rebuild its client, see [credential rotation](../../../usage/api-concepts.md#credentials-and-cluster-identity).
+
+## Runtime options
+
+The konnector runs against the **consumer** kubeconfig, or uses its in-cluster ServiceAccount. Provider credentials come from each Connection's referenced Secret, not from the konnector's own Kubernetes identity.
+
+```bash
+go run ./cmd/konnector --help
+```
+
+| Flag | Default | Purpose |
+| --- | --- | --- |
+| `--metrics-bind-address` | `:8085` | Metrics listener, `0` disables it |
+| `--health-probe-bind-address` | `:8081` | `/healthz` and `/readyz` listener |
+| `--leader-elect` | `false` | Run only the elected replica's consumer controllers and provider engagement |
+| `--leader-election-id` | `konnector.kbind.io` | Leader-election Lease name |
+
+The binary also exposes controller-runtime's kubeconfig and Zap logging flags, use its `--help` output for those options. There are no flags for legacy isolation modes, namespace remapping, schema selection, or per-provider credentials: schema and credential configuration belongs in the [core API](../../../reference/crd/index.md#corekbindiov1alpha1).
+
+The chart forces leader election when `replicaCount > 1`. Outside the chart, enable it explicitly for multiple replicas. Probe success indicates the process is serving, not that every Connection or binding is Ready. Inspect those objects' conditions to assess synchronization health.
+
+See [Konnector configuration](../index.md) for chart values and consumer RBAC, and [troubleshooting](../../../usage/troubleshooting.md) for runtime status.
+
+Implementation: [process setup](https://github.com/kbind-dev/kbind/blob/v2-next/cmd/konnector/main.go) and [Connection provider](https://github.com/kbind-dev/kbind/blob/v2-next/engine/provider/connection_provider.go).
diff --git a/docs/content/developers/konnector/controllers/synchronization.md b/docs/content/developers/konnector/controllers/synchronization.md
new file mode 100644
index 000000000..f4fe957ba
--- /dev/null
+++ b/docs/content/developers/konnector/controllers/synchronization.md
@@ -0,0 +1,136 @@
+# Synchronization Controllers
+
+The managed-CRD controller watches kbind-managed `CustomResourceDefinitions` and their `Connections` in the **consumer cluster**. It starts a dynamic instance syncer for each selected group/version/resource, or GVR. Both are implemented in `engine/sync`.
+
+They are responsible for:
+
+* Starting, stopping, and rebuilding syncers as CRDs and provider connections change.
+* Selecting instances covered by a Ready ClusterBinding or namespaced Binding.
+* Applying consumer spec to owned provider objects and copying provider status back.
+* Reporting ownership conflicts without overwriting foreign objects.
+* Deleting owned provider copies before releasing consumer instance finalizers.
+
+## Overview
+
+The managed-CRD controller resolves the provider from the CRD's `core.kbind.io/connection` annotation. A missing annotation or an API error is returned for retry.
+
+```mermaid
+flowchart TD
+ start(["Managed CRD or Connection event"])
+ crd{"CRD exists and is not deleting?"}
+ connection{"Connection Ready and localClusterUID set?"}
+ engaged{"Provider client and cache engaged?"}
+ current{"Existing syncer has current generation and the same provider instance?"}
+ stop_old(["Stop the previous syncer"])
+ version(["Choose storage or served version Build consumer informer and instance controller"])
+ watches(["Watch consumer and provider instances Watch bindings covering this API"])
+ run(["Start controller and informer Wait for cache sync and record syncer"])
+ stop_syncer(["Stop existing syncer"])
+ wait_provider(["Stop existing syncer Requeue after 2 seconds"])
+ stop(["Stop"])
+
+ start --> crd
+ crd -->|no| stop_syncer
+ crd -->|yes| connection
+ connection -->|no| stop_syncer
+ connection -->|yes| engaged
+ engaged -->|no| wait_provider
+ engaged -->|yes| current
+ current -->|yes| stop
+ current -->|no| stop_old
+ stop_old --> version
+ version --> watches
+ watches --> run
+ run --> stop
+ stop_syncer --> stop
+ wait_provider --> stop
+```
+
+The generation check detects schema changes. Comparing the provider instance also detects re-engagement after an outage, so a syncer cannot keep the old client/cache simply because the CRD generation is unchanged.
+
+## Instance reconciliation
+
+Consumer events, binding events, and provider cache events enqueue instances. Provider events are filtered by consumer-cluster UID and mapped back to the consumer object key. Reads of provider objects use the engaged cluster's API reader, while writes use its client.
+
+```mermaid
+flowchart TD
+ start(["Consumer, provider, or binding event"])
+ object{"Consumer object exists?"}
+ resolve(["Resolve covering binding ClusterBinding before namespaced Binding"])
+ deleting{"Consumer object deleting?"}
+ cleanup(["Run instance deletion"])
+ bound{"Covering binding Ready?"}
+ map_key(["Map consumer key to provider key"])
+ finalizer{"Syncer finalizer already present?"}
+ add_finalizer(["Add finalizer and requeue"])
+ target(["Read provider object"])
+ ownership{"Provider target ownership?"}
+ adopt{"conflictPolicy is Adopt?"}
+ conflict(["Record conflict annotation and Warning Event"])
+ namespace(["Ensure provider namespace for a new namespaced object"])
+ apply(["Clear old conflict marker Apply spec and ownership markers"])
+ status(["Read provider object again Copy status if present"])
+ stop(["Stop"])
+
+ start --> object
+ object -->|no| stop
+ object -->|yes| resolve
+ resolve --> deleting
+ deleting -->|yes| cleanup
+ cleanup --> stop
+ deleting -->|no| bound
+ bound -->|no| stop
+ bound -->|yes| map_key
+ map_key --> finalizer
+ finalizer -->|no| add_finalizer
+ add_finalizer --> stop
+ finalizer -->|yes| target
+ target --> ownership
+ ownership -->|absent| namespace
+ namespace --> apply
+ ownership -->|ours| apply
+ ownership -->|no ownership markers| adopt
+ ownership -->|owned by another| conflict
+ adopt -->|yes| apply
+ adopt -->|no| conflict
+ conflict --> stop
+ apply --> status
+ status --> stop
+```
+
+Ownership requires both the consumer-cluster UID and consumer-object UID. `Adopt` accepts only markerless objects, never objects owned by another consumer/object. Spec apply uses server-side apply with forced field ownership. An absent provider status does not clear an existing consumer status.
+
+Forbidden namespace creation or spec apply emits a Warning Event and retries after 30 seconds. Other API errors are returned for retry. Successful instance reconciliation schedules a ten-minute backstop, with watches and the consumer informer's resync providing additional triggers.
+
+## Instance deletion
+
+```mermaid
+flowchart TD
+ start(["Deleting consumer object"])
+ finalizer{"Syncer finalizer present?"}
+ orphan{"deletion-policy is Orphan?"}
+ read_provider(["Read mapped provider object"])
+ owned{"Provider copy exists and is ours?"}
+ delete_copy(["Request provider deletion Requeue after 2 seconds"])
+ release(["Remove consumer syncer finalizer"])
+ stop(["Stop"])
+
+ start --> finalizer
+ finalizer -->|no| stop
+ finalizer -->|yes| orphan
+ orphan -->|yes| release
+ orphan -->|no| read_provider
+ read_provider --> owned
+ owned -->|yes| delete_copy
+ delete_copy --> stop
+ owned -->|no| release
+ release --> stop
+```
+
+This path runs even if the binding is no longer Ready. An owned provider object must disappear before its consumer finalizer is released. Provider read/delete errors are retried, not treated as absence. Foreign objects are left untouched.
+
+The stock `Mapper` preserves scope, namespace, and name. It is a compile-time extension point for instance keys, not a CRD isolation flag. Related-resource sync remains in binding reconciliation and does not use that mapper.
+
+See [Resource Synchronization](../../../usage/synchronization.md) for conflict policy, finalizers, related-resource selection, and supported behavior.
+
+Implementation: [managed-CRD controller](https://github.com/kbind-dev/kbind/blob/v2-next/engine/sync/crd_controller.go), [binding resolution](https://github.com/kbind-dev/kbind/blob/v2-next/engine/sync/resolve.go), and [instance syncer](https://github.com/kbind-dev/kbind/blob/v2-next/engine/sync/syncer.go).
diff --git a/docs/content/developers/konnector/index.md b/docs/content/developers/konnector/index.md
new file mode 100644
index 000000000..6f07a2f6a
--- /dev/null
+++ b/docs/content/developers/konnector/index.md
@@ -0,0 +1,101 @@
+# konnector
+
+The konnector runs on the consumer and synchronizes APIs from provider clusters. In v2 it is the only running component required by the core.
+
+- [Konnector](controllers/konnector.md): provider engagement, managers, and runtime options.
+- [Connection controller](controllers/connection.md): credentials, identity, discovery, and heartbeat.
+- [Binding controllers](controllers/binding.md): API selection, schema delivery, and cleanup.
+- [Synchronization](controllers/synchronization.md): per-API spec/status watches and ownership.
+
+See [Architecture](../architecture.md) for the package layout.
+
+## Deployment
+
+Start with [Installation with Helm](../../setup/helm.md). The konnector runs in the consumer cluster and connects directly to provider Kubernetes APIs. It needs no kbind controller or kbind CRDs on a plain Kubernetes provider.
+
+### Local kind images
+
+For an existing kind consumer, build and load the image rather than pushing it:
+
+```bash
+export IMAGE_REPOSITORY=kbind-local/konnector
+export IMAGE_TAG="git-$(git rev-parse --short=12 HEAD)"
+make image IMAGE="$IMAGE_REPOSITORY:$IMAGE_TAG"
+kind load docker-image "$IMAGE_REPOSITORY:$IMAGE_TAG" --name consumer
+```
+
+Use those values with the Helm command and select `kind-consumer` as its context. Build for the consumer nodes' architecture and use a new tag for uncommitted changes. For a private registry, configure `imagePullSecrets`.
+
+The Pod must be able to reach each provider endpoint using the CA and credentials in its Connection Secret. A workstation's `127.0.0.1` kind endpoint is not reachable from a consumer Pod. With two kind clusters on the same Docker network, use an address reachable across that network and covered by the provider serving certificate. Keep TLS verification enabled.
+
+### Chart configuration
+
+| Value | Default | Meaning |
+| --- | --- | --- |
+| `image.repository`, `image.tag` | repository plus placeholder appVersion | Set both explicitly for this branch. |
+| `installCRDs` | `true` | Install the three core CRDs as Helm templates. |
+| `replicaCount` | `1` | Number of Pods, only one should actively reconcile. |
+| `leaderElect` | `false` | Automatically forced on when `replicaCount > 1`. |
+| `leaderElectionID` | `konnector.kbind.io` | Consumer-side leader-election Lease name. |
+| `rbac.boundResourceGroups` | `["*"]` | Consumer API groups the engine can synchronize. |
+| `metrics.port` | `8085` | Controller-runtime metrics endpoint. |
+| `healthProbe.port` | `8081` | `/healthz` and `/readyz` endpoints. |
+| `extraArgs` | `[]` | Additional konnector arguments, including logging flags. |
+
+For multiple replicas, the chart supplies leader-election RBAC and enables election. Do not deploy independent active releases against the same consumer. The probes are process checks, not proof that a Connection or instance is syncing.
+
+## RBAC and credentials
+
+### Consumer permissions
+
+The chart manages core objects and their status/finalizers, CRDs, credential Secrets, namespaces, and Events. By default, it also grants ordinary CRUD/watch operations on **all API groups and resources**. Review this broad access before a shared deployment.
+
+For example, narrow the configurable resource rule to Widget APIs:
+
+```yaml
+rbac:
+ boundResourceGroups:
+ - example.org
+```
+
+Pass the values with `helm ... -f your-values.yaml`. This does not narrow the chart's separate access to core kbind objects, CRDs, or credential Secrets.
+
+For related resources, supply the additional consumer RBAC they need. The chart's fixed Secret rule allows reads and updates for credentials, but not creation/deletion of related Secret copies. There is no fixed ConfigMap rule. Add narrowly scoped rules for the related resource kinds and direction rather than granting all core resources through the empty API group.
+
+A namespaced Binding restricts which instances synchronize, not informer permissions. Current instance informers watch cluster-wide on both sides, so a namespace-only Role is not sufficient.
+
+### Provider permissions
+
+Use dedicated credentials for the intended provider boundary, not a production administrator kubeconfig.
+
+| Operation | Required provider access |
+| --- | --- |
+| Identify plain Kubernetes | Read Namespace `kube-system`. |
+| Identify a kcp logical cluster | Read `logicalclusters.core.kcp.io/cluster`, a forbidden result is not ignored. |
+| Discover CRD exports | List CRDs and get selected CRDs in `apiextensions.k8s.io`. |
+| Discover OpenAPI exports | Read discovery endpoints and `/openapi/v3` documents. |
+| Sync instances | Cluster-wide get/list/watch for the selected APIs, create/patch/update/delete for targets. |
+| Materialize a namespace | Create namespaces, the engine attempts a create even if the namespace may already exist. |
+| Sync related objects | List/read selected sources, read/list/create/patch/delete targets according to direction. |
+| Heartbeat | Get/create/update Leases in the kubeconfig context namespace, or `kbind` when unset. |
+
+Provider RBAC and network boundaries control access. Export labels are not authorization. Namespace/name mapping is identity-only, credentials with a default namespace do not redirect objects into it.
+
+Store the provider kubeconfig in a consumer Secret and reference its explicit namespace/name/key from a Connection. The chart does not create credentials. See [API concepts](../../usage/api-concepts.md#credentials-and-cluster-identity) for immutable references, identity pinning, and rotation.
+
+## Manage CRDs outside Helm
+
+For a GitOps-managed lifecycle, install the core manifests before the release:
+
+```bash
+kubectl --context="$CONSUMER_CONTEXT" apply \
+ -f sdk/config/crd/core.kbind.io_connections.yaml \
+ -f sdk/config/crd/core.kbind.io_clusterbindings.yaml \
+ -f sdk/config/crd/core.kbind.io_bindings.yaml
+```
+
+Set `installCRDs: false` from the first Helm installation. Do not switch an existing release from `true` to `false` without planning the migration. These are rendered templates, not Helm's retained `crds/` directory, so removing them from a release can delete them and their custom resources.
+
+The rest of `sdk/config/crd` contains optional catalog and IAM CRDs. Installing the three named files keeps a core-only deployment minimal.
+
+Before uninstalling, unbind while the controller and credentials still work, delete Connections after bindings drain, and remove credential Secrets after their finalizers are released. Removing the controller or CRDs first bypasses normal teardown. See [Unbinding and deletion](../../usage/synchronization.md#unbinding-and-deletion).
diff --git a/docs/content/developers/publishing-a-release.md b/docs/content/developers/publishing-a-release.md
new file mode 100644
index 000000000..452cfb87b
--- /dev/null
+++ b/docs/content/developers/publishing-a-release.md
@@ -0,0 +1,19 @@
+# Publishing a release
+
+v2 releases are driven by version tags. The Image workflow publishes versioned `konnector` and `backend` images plus the `konnector-v2` and `backend-v2` OCI charts. Final tags also publish CLI archives, prerelease tags do not update krew.
+
+## Release candidates
+
+From the intended checkout, inspect the next release-candidate tag:
+
+```bash
+go run ./cmd/release -remote upstream -dry-run
+```
+
+Only when ready to create and publish it, use the helper's `-push` option. The remote must point to the intended release repository.
+
+See the maintained [release procedure](https://github.com/kbind-dev/kbind/blob/v2-next/docs/RELEASING.md) for manual tags, final releases, and maintenance branches.
+
+## Documentation
+
+The v2 docs publisher uses `v2` without changing the site's root redirect or `latest` alias. Tags do not automatically promote v2 documentation to the default. Local build and publishing instructions live in `docs/README.md`.
diff --git a/docs/content/developers/testing-changes.md b/docs/content/developers/testing-changes.md
new file mode 100644
index 000000000..0e11e932f
--- /dev/null
+++ b/docs/content/developers/testing-changes.md
@@ -0,0 +1,73 @@
+---
+description: >
+ How to test changes made to kbind in your development environment.
+title: Testing Changes
+---
+
+# Testing code changes
+
+Use the v2 source branch rather than `main`, which still describes the 0.x implementation:
+
+```bash
+git clone --branch v2-next https://github.com/kbind-dev/kbind.git
+cd kbind
+make build
+make konnector backend bind
+```
+
+The root module builds the engine, backend, and CLI. `sdk/` is a separate Go module with a local `replace` in the root `go.mod`, a `go.work` file is not required. Use the Go version declared in `go.mod` (currently 1.26.2). The binaries are `bin/konnector`, `bin/backend`, and `bin/bind`.
+
+## Run a change locally
+
+Start with the [two-cluster walkthrough](dev-environment/kind.md). Running the konnector on your workstation against the consumer kubeconfig makes engine changes easy to iterate without rebuilding an image.
+
+The [backend setup](backend/index.md) covers a local gateway with mock OIDC. Never use mock authentication on an exposed production endpoint. The gateway UI is embedded from `web/`, the v2 UI does not require the old npm frontend build.
+
+For in-cluster runs, build and load or push explicitly tagged images:
+
+```bash
+make image IMAGE=ghcr.io/kbind-dev/konnector:dev
+make image-backend BACKEND_IMAGE=ghcr.io/kbind-dev/backend:dev
+```
+
+These commands build locally, they do not publish the images. Use the `deploy/charts/konnector-v2` and `deploy/charts/backend-v2` charts and override their image tags to match. The legacy chart directory names without `-v2` do not exist in this branch.
+
+## Tests
+
+```bash
+make test
+make test-e2e
+```
+
+Unit tests run without external Kubernetes setup. The envtest suite starts two in-process Kubernetes API servers and the real engine controllers. `make test-e2e` obtains API-server binaries through `setup-envtest`.
+
+Run related tests together when iterating:
+
+```bash
+KUBEBUILDER_ASSETS="$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.21 use 1.34.1 -p path)" \
+ go test ./test/e2e -run 'TestSlimCore(HappyCase|NamespacedBinding)$' \
+ -count=1 -timeout=600s
+```
+
+The suite covers schema policies, OpenAPI and kcp-like discovery, related resources, conflicts, cleanup, and stop-on-disengage. `TestBackendFullLoop` adds gateway issuance, bundle pickup, provider-fenced credentials, heartbeat, and revocation. Envtest is not a real provider operator or a full kcp deployment.
+
+The repository also has `make test-e2e-kind` for a containerized run with Docker, kind, Helm, and kubectl. The Tilt helper still refers to the old chart directory names, use the step-by-step quickstart instead of relying on `make tilt` for this preview.
+
+### Demo scripts
+
+`make demo` runs `hack/demo.sh`, which creates or reuses `kbind-provider` and `kbind-consumer` kind clusters and writes administrator kubeconfigs under `/tmp`. It prints a command for running the konnector on your workstation.
+
+
+`hack/e2e.sh` builds and loads an image, installs the konnector chart, and deletes its named clusters on exit unless `KEEP=1`. Inspect it before use. Its `NAMESPACE` override does not rewrite the fixed `kbind` Secret reference in `config/samples/binding.yaml`. For explicit cluster selection and manual cleanup, use the [kind walkthrough](dev-environment/kind.md).
+
+## API changes
+
+Edit types in `sdk/apis/{core,catalog,iam}/v1alpha1`, then regenerate the deep-copy code, CRDs, and packaged manifests:
+
+```bash
+make helm-sync-crds
+```
+
+This includes the core CRDs embedded by `pkg/konnectorinstall`, service CRDs in `pkg/servicecrds`, and both charts' copies. Run `make generate-docs` to refresh local API and CLI references.
+
+Release tags publish images and OCI Helm charts with explicit versions. Final tags also trigger CLI release artifacts, prerelease tags do not. See the [release procedure](https://github.com/kbind-dev/kbind/blob/v2-next/docs/RELEASING.md).
diff --git a/docs/content/images/high-level.png b/docs/content/images/high-level.png
new file mode 100644
index 000000000..4cbbcf430
Binary files /dev/null and b/docs/content/images/high-level.png differ
diff --git a/docs/content/images/mangodb-bind-ui.png b/docs/content/images/mangodb-bind-ui.png
new file mode 100644
index 000000000..bd12cef56
Binary files /dev/null and b/docs/content/images/mangodb-bind-ui.png differ
diff --git a/docs/content/images/overview.png b/docs/content/images/overview.png
new file mode 100644
index 000000000..0fb82e207
Binary files /dev/null and b/docs/content/images/overview.png differ
diff --git a/docs/content/index.md b/docs/content/index.md
new file mode 100644
index 000000000..77c5c1dc9
--- /dev/null
+++ b/docs/content/index.md
@@ -0,0 +1,70 @@
+# kbind Documentation
+
+## Overview
+
+kbind (formerly known as kube-bind) is a project that aims to provide better support for service providers and consumers that reside in distinct Kubernetes clusters. We are actively working towards a stable release, and welcome feedback from the community.
+
+This documentation covers **v2**. The [0.x documentation](https://docs.kbind.dev/latest/) remains the default. See [Moving from 0.x](usage/migration.md) before upgrading an existing installation.
+
+
+
+The diagram illustrates provider/consumer separation. In v2, authentication through an identity provider is optional, and namespace isolation must be arranged by the deployment rather than automatic namespace remapping.
+
+- A service provider defines its API contract in terms of CRDs and credential RBAC, and labels the CRDs for export.
+- Service consumers identify the services they want to consume using the optional catalog, CLI or Web UI, or apply core bindings directly through GitOps.
+- The service CRDs get installed in the service consumer clusters, with objects of the defined kinds written and read by the service consumers.
+- The service provider indirectly reads and writes those objects as the interface to the service that it provides.
+- The service provider does not inject controllers/operators into the service consumer's cluster.
+- A single vendor-neutral, OpenSource agent - `konnector` per consumer cluster connects it with the requested services.
+
+The v2 core consumes a kubeconfig Secret, a `Connection`, and `ClusterBinding` or namespaced `Binding` objects. The backend is optional, and the shipped syncer preserves resource scope, namespace, and name.
+
+## v2 Architecture
+
+```mermaid
+flowchart LR
+ subgraph Consumer
+ B["Secret + Connection + bindings"]
+ K[Konnector]
+ C[Custom resources]
+ B --> K
+ C --> K
+ end
+ subgraph Provider
+ P[Custom resources]
+ O[Service operator]
+ G["Optional backend: catalog, auth, issuer, reaper"]
+ P <--> O
+ end
+ K -- "spec up" --> P
+ P -- "status down" --> K
+ K --> C
+ G -. "one-apply bundle" .-> B
+```
+
+The optional backend produces the bundle, synchronization runs directly between the consumer's konnector and the provider API server, not through the gateway. See [Architecture Overview](developers/architecture.md) for the controller layout.
+
+## Getting Started
+
+- **[Quickstart](setup/quickstart.md)** - Get up and running with kbind quickly
+- **[Setup Guide](setup/index.md)** - Complete installation and deployment options
+- **[Usage Guide](usage/index.md)** - Learn the core concepts and APIs
+
+## Contributing
+
+We ❤️ our contributors! If you're interested in helping us out, please head over to our [Contributing](contributing/index.md) guide.
+
+## Getting in touch
+
+There are several ways to communicate with us:
+
+- The [`#kbind-dev` channel](https://kubernetes.slack.com/archives/C046PRXNJ4W) in the [Kubernetes Slack workspace](https://slack.k8s.io).
+- Our mailing lists:
+ - [kube-bind-dev](https://groups.google.com/g/kube-bind-dev) for development discussions.
+- Our bi-weekly community meetings, every second Thursday at 11am EST (5pm CET).
+ - By joining the [kube-bind-dev mailing list](https://groups.google.com/g/kube-bind-dev), you should receive an invite.
+ - See our [community meeting notes document](https://docs.google.com/document/d/1qztpKOmdZu5iWq_4N9n3AZpcAPuPhBiGNbje5GPg0iM) for upcoming and past agendas.
+
+
+
+See the [community page](community/index.md) for more details.
diff --git a/docs/content/reference/.gitignore b/docs/content/reference/.gitignore
new file mode 100644
index 000000000..831e0495e
--- /dev/null
+++ b/docs/content/reference/.gitignore
@@ -0,0 +1,2 @@
+cli/
+crd/
diff --git a/docs/content/reference/.pages b/docs/content/reference/.pages
new file mode 100644
index 000000000..455fd44f2
--- /dev/null
+++ b/docs/content/reference/.pages
@@ -0,0 +1,4 @@
+nav:
+ - index.md
+ - CLI: cli
+ - CRD: crd
diff --git a/docs/content/reference/index.md b/docs/content/reference/index.md
new file mode 100644
index 000000000..3368150f0
--- /dev/null
+++ b/docs/content/reference/index.md
@@ -0,0 +1,14 @@
+# Reference
+
+These references describe the code in the v2 checkout used to build this site. Kubernetes API pages are generated from `sdk/apis`, the CLI page is generated from `cli/cmd`. No generator fetches API definitions from the 0.x `main` branch.
+
+| Surface | Reference |
+| --- | --- |
+| Consumer connection, binding, and schema policy | [core.kbind.io](crd/index.md#corekbindiov1alpha1) |
+| Provider catalog offerings and collections | [catalog.kbind.io](crd/index.md#catalogkbindiov1alpha1) |
+| Provider credential issuance record | [iam.kbind.io](crd/index.md#iamkbindiov1alpha1) |
+| CLI command syntax and flags | [CLI](cli/index.md) |
+| Optional gateway endpoints and backend options | [Backend](../developers/backend/http/index.md) |
+| Consumer agent options | [Konnector](../developers/konnector/controllers/konnector.md) |
+
+The [API concepts guide](../usage/api-concepts.md) explains which objects to create and how they fit together. Generated type descriptions are not a promise that every syntactically valid combination is supported, consult the [synchronization limits](../usage/synchronization.md).
diff --git a/docs/content/setup/.pages b/docs/content/setup/.pages
new file mode 100644
index 000000000..e7c89b2c2
--- /dev/null
+++ b/docs/content/setup/.pages
@@ -0,0 +1,5 @@
+nav:
+ - index.md
+ - kubectl-plugin.md
+ - quickstart.md
+ - helm.md
\ No newline at end of file
diff --git a/docs/content/setup/helm.md b/docs/content/setup/helm.md
new file mode 100644
index 000000000..525b39b2f
--- /dev/null
+++ b/docs/content/setup/helm.md
@@ -0,0 +1,56 @@
+---
+description: >
+ Install kbind on an existing Kubernetes cluster via the Helm charts.
+---
+
+# Installation with Helm
+
+The konnector chart runs in the consumer cluster. The optional backend chart provides a catalog, authentication, and credentials on the provider.
+
+## Prerequisites
+
+- A Kubernetes consumer cluster and Helm 3.
+- A v2 source checkout and an image built from that checkout.
+- A provider API reachable from the consumer cluster.
+
+This preview uses the local charts. Do not use a 0.x chart or assume a `latest` image contains v2.
+
+## Install the Konnector
+
+From the repository root, build and push an image to a registry you control:
+
+```bash
+export IMAGE_REPOSITORY=registry.example.com/your-project/konnector
+export IMAGE_TAG="git-$(git rev-parse --short=12 HEAD)"
+make image IMAGE="$IMAGE_REPOSITORY:$IMAGE_TAG"
+docker push "$IMAGE_REPOSITORY:$IMAGE_TAG"
+```
+
+Replace the registry above. Use a new tag when rebuilding uncommitted changes. For local kind clusters, load the image with `kind load docker-image` instead.
+
+Install into your consumer context:
+
+```bash
+export CONSUMER_CONTEXT=kind-consumer
+helm upgrade --install konnector ./deploy/charts/konnector-v2 \
+ --kube-context "$CONSUMER_CONTEXT" \
+ --namespace kbind --create-namespace \
+ --set-string image.repository="$IMAGE_REPOSITORY" \
+ --set-string image.tag="$IMAGE_TAG" \
+ --set image.pullPolicy=IfNotPresent \
+ --wait --timeout 180s
+```
+
+The chart installs the three core CRDs, RBAC, and the konnector Deployment. Review its broad default permissions before using a shared cluster. Provider credentials belong in a consumer Secret referenced by a Connection.
+
+See [API concepts](../usage/api-concepts.md) for binding manifests and the [konnector guide](../developers/konnector/index.md) for configuration, networking, and credential permissions.
+
+## Install the Backend
+
+Service providers can use `deploy/charts/backend-v2`. Follow the [backend setup](../developers/backend/index.md) for image selection, OIDC, session-key Secrets, and Helm values, then [publish an offering](../usage/catalog.md#publish-an-offering).
+
+The backend is optional. The core can use provider credentials and bindings directly without it.
+
+## Upgrade and Uninstall
+
+Repeat `helm upgrade --install` with the new image tag to upgrade. Before uninstalling, [unbind resources](../usage/synchronization.md#unbinding-and-deletion) while the konnector and provider credentials still work. With `installCRDs: true`, Helm uninstall also removes the core CRDs. See [CRD lifecycle](../developers/konnector/index.md#manage-crds-outside-helm) before changing who manages them.
diff --git a/docs/content/setup/index.md b/docs/content/setup/index.md
new file mode 100644
index 000000000..bc92f6e27
--- /dev/null
+++ b/docs/content/setup/index.md
@@ -0,0 +1,12 @@
+# Setting Up kube-bind
+
+kube-bind supports multiple deployment scenarios and backend providers to meet different requirements.
+
+## Setup Options
+
+### Standard Kubernetes Setup
+
+- **[kubectl plugin](kubectl-plugin.md)**: Install and use the kubectl-bind plugin
+- **[Quickstart](quickstart.md)**: Get started quickly with a minimal setup
+- **[Helm Deployment](helm.md)**: Production deployment using Helm charts
+- **[Development Environment](../developers/dev-environment/index.md)**: Local development environment if you want to experiment with kube-bind on your machine
diff --git a/docs/content/setup/kubectl-plugin.md b/docs/content/setup/kubectl-plugin.md
new file mode 100644
index 000000000..a3af9935f
--- /dev/null
+++ b/docs/content/setup/kubectl-plugin.md
@@ -0,0 +1,66 @@
+---
+description: >
+ Install and use the kubectl bind plugin.
+---
+
+# kubectl bind Plugin
+
+The `kubectl bind` plugin is the command-line interface for interacting with kbind services. It connects to remote service providers, browses their catalogs, and binds APIs into your cluster.
+
+## Installation
+
+=== "Krew"
+
+ [Krew](https://krew.sigs.k8s.io/) installs released versions of the plugin, not this unreleased v2 preview. Use **Manual Build** for this preview rather than installing a 0.x release.
+
+=== "Manual Build"
+
+ Build and install from source using the Go version declared in `go.mod`:
+
+ ```bash
+ git clone --branch v2-next https://github.com/kbind-dev/kbind.git
+ cd kbind
+ make bind
+ mkdir -p "$HOME/.local/bin"
+ install -m 0755 bin/bind "$HOME/.local/bin/kubectl-bind"
+ export PATH="$HOME/.local/bin:$PATH"
+ kubectl bind --help
+ ```
+
+ Keep `$HOME/.local/bin` on your shell's `PATH`. Installing the build output as `kubectl-bind` makes it available as `kubectl bind`.
+
+=== "Binary Download"
+
+ Published CLI archives are available on the [releases page](https://github.com/kbind-dev/kbind/releases). Choose a v2 release explicitly, not the latest 0.x release. For this unreleased branch, use **Manual Build**.
+
+ After extracting an archive for your operating system and architecture, install its `kubectl-bind` binary:
+
+ ```bash
+ mkdir -p "$HOME/.local/bin"
+ install -m 0755 kubectl-bind "$HOME/.local/bin/kubectl-bind"
+ export PATH="$HOME/.local/bin:$PATH"
+ ```
+
+## Basic Usage
+
+With the [v2 konnector installed](helm.md) in your consumer cluster:
+
+```bash
+kubectl bind login https://my-kube-bind-server.example.com
+kubectl bind catalog
+kubectl bind export widgets --kubeconfig=./consumer.kubeconfig --install-konnector=false
+kubectl --kubeconfig=./consumer.kubeconfig get connections,clusterbindings
+```
+
+Replace `widgets` with an Export from the catalog. To use the Web UI, open the provider URL in your browser. In v2, binding uses `export ` rather than bare `kubectl bind`.
+
+See the [CLI reference](../reference/cli/index.md) for all commands and the [catalog guide](../usage/catalog.md) for sessions, credentials, and bundle handling.
+
+## Quick Start
+
+1. Build and install the v2 plugin using **Manual Build**.
+2. Install the consumer konnector using the [Helm guide](helm.md).
+3. Log in to your provider with `kubectl bind login `.
+4. Browse with `kubectl bind catalog` and bind an Export with `kubectl bind export `.
+
+For a local example, follow the [Quickstart Guide](quickstart.md).
diff --git a/docs/content/setup/quickstart.md b/docs/content/setup/quickstart.md
new file mode 100644
index 000000000..88bcd0174
--- /dev/null
+++ b/docs/content/setup/quickstart.md
@@ -0,0 +1,85 @@
+---
+description: >
+ Get started with kbind.
+---
+
+# Quickstart
+
+## Prerequisites
+
+- [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)
+- [kubectl bind plugin](kubectl-plugin.md) installed
+
+## Start with kbind
+
+### Quick Development Setup
+
+For a local development environment, follow the [kind setup guide](../developers/dev-environment/kind.md). It creates a provider cluster and a consumer cluster, starts the backend and konnector, and adds the Widget API to the catalog. Docker, kind, and Go are required.
+
+After the development environment is ready, you can:
+
+1. **Authenticate to the provider cluster:**
+
+ ```bash
+ kubectl bind login http://localhost:8080
+ ```
+
+ This opens a browser for authentication and saves the session.
+
+2. **Bind an API service from provider to consumer:**
+
+ ```bash
+ kubectl bind catalog
+ kubectl bind export widgets --kubeconfig=.kbind-quickstart/consumer.kubeconfig \
+ --install-konnector=false
+ ```
+
+ The development setup already runs the konnector, so skip installing another copy. You can also browse the catalog at `http://localhost:8080`.
+
+3. **Verify bound resources:**
+
+ ```bash
+ export KUBECONFIG="$PWD/.kbind-quickstart/consumer.kubeconfig"
+ kubectl get connections,clusterbindings
+ kubectl wait --for=condition=Established crd/widgets.example.org --timeout=120s
+ ```
+
+4. **Create an example resource:**
+
+ ```bash
+ kubectl apply -f - <<'EOF'
+ apiVersion: example.org/v1
+ kind: Widget
+ metadata:
+ name: my-widget
+ namespace: default
+ spec:
+ size: large
+ EOF
+ ```
+
+5. **Check the resource synced to the provider:**
+
+ ```bash
+ kubectl --kubeconfig=.kbind-quickstart/provider.kubeconfig -n default \
+ wait --for=create widget/my-widget --timeout=120s
+ kubectl --kubeconfig=.kbind-quickstart/provider.kubeconfig -n default get widgets
+ ```
+
+For other service APIs, see the [integration examples](../usage/integrations/index.md). When finished, follow the development guide's [cleanup](../developers/dev-environment/kind.md#cleanup).
+
+### Production Deployment
+
+For an in-cluster deployment, use the [Helm charts](helm.md) to install the consumer konnector and optional provider backend.
+
+Once you have a backend running, either locally or in a cluster, you can connect to it:
+
+### Connect to kbind Server
+
+```bash
+kubectl bind login https://my-kube-bind-server.example.com
+```
+
+### Open kbind Web UI and bind services
+
+Open your provider's URL in a browser to browse the catalog and bind services. For the same workflow from the CLI, use `kubectl bind catalog` and `kubectl bind export `.
diff --git a/docs/content/usage/.pages b/docs/content/usage/.pages
new file mode 100644
index 000000000..90a116ac8
--- /dev/null
+++ b/docs/content/usage/.pages
@@ -0,0 +1,9 @@
+nav:
+ - index.md
+ - api-concepts.md
+ - synchronization.md
+ - catalog.md
+ - gitops.md
+ - integrations
+ - migration.md
+ - troubleshooting.md
diff --git a/docs/content/usage/api-concepts.md b/docs/content/usage/api-concepts.md
new file mode 100644
index 000000000..ca636a500
--- /dev/null
+++ b/docs/content/usage/api-concepts.md
@@ -0,0 +1,191 @@
+---
+title: API Concepts
+description: >
+ Core API types, their relationships, and cross-cluster service binding.
+weight: 210
+---
+
+# kbind API Concepts
+
+This guide provides an overview of kbind's core API types, their relationships, and how they work together to enable cross-cluster service binding.
+
+## Overview
+
+The v2 slim core is three consumer-side resources in **`core.kbind.io/v1alpha1`**. It connects Kubernetes APIs using declarative objects, it is not an API request proxy.
+
+| Kind | Scope | Responsibility |
+| --- | --- | --- |
+| `Connection` | Cluster | Reference provider credentials, pin cluster identity, discover exported APIs, and choose schema policy. |
+| `ClusterBinding` | Cluster | Activate synchronization for listed APIs across the consumer. |
+| `Binding` | Namespace | Activate synchronization for listed namespaced APIs in this namespace. |
+
+The konnector reconciles them in the consumer. A plain Kubernetes provider needs its own API/controller, credentials and RBAC, and exported CRD labels, not these core CRDs. The optional [backend](../developers/backend/index.md) and [catalog](catalog.md) add onboarding and discovery workflows without changing this core contract.
+
+See the [core API reference](../reference/crd/index.md#corekbindiov1alpha1) for field definitions. These pages describe the current, unreleased implementation, policy limitations below matter even where the API types describe a broader intent.
+
+## Connection
+
+**Purpose**: Establish the provider link and choose schema policy. **Used by**: Service consumers. **Scope**: Cluster-scoped.
+
+### Structure
+
+```yaml
+apiVersion: core.kbind.io/v1alpha1
+kind: Connection
+metadata:
+ name: provider
+spec:
+ kubeconfigSecretRef:
+ namespace: kbind
+ name: provider-kubeconfig
+ key: kubeconfig
+ schema:
+ source: CRD
+ pullPolicy: Bound
+ updatePolicy: Always
+---
+apiVersion: core.kbind.io/v1alpha1
+kind: Binding
+metadata:
+ name: widgets
+ namespace: team-a
+spec:
+ connectionRef:
+ name: provider
+ apis:
+ - name: widgets.example.org
+ conflictPolicy: Fail
+```
+
+Create `team-a` separately. The Connection reference has no namespace because Connections are cluster-scoped. Each `apis[].name` is `.`, not a Kind, a version, or a URL.
+
+The Connection can exist before its Secret, and the Binding before its Connection, controllers retry when dependencies appear. The CRD definitions themselves must already exist.
+
+### Credentials and cluster identity
+
+`spec.kubeconfigSecretRef` is the only credential reference in the core. It contains a required Secret namespace and name and an optional key defaulting to `kubeconfig`. The Secret may be in any namespace the konnector is authorized to read, it is not implicitly resolved in the Binding namespace.
+
+The **reference is immutable**, but the Secret data can be updated. Keep credentials self-contained and usable in the konnector's environment. A kubeconfig that relies on your workstation's files, exec plugin, or interactive login is not automatically usable inside the stock container.
+
+On first successful connection, the engine records:
+
+- `status.remoteClusterUID`: provider identity.
+- `status.localClusterUID`: consumer identity.
+
+It identifies a cluster using the `core.kcp.io/v1alpha1` LogicalCluster named `cluster` when available, otherwise the `kube-system` Namespace UID. A forbidden identity read is reported rather than silently skipped. The API makes each recorded UID immutable once set.
+
+Subsequent reconciliations compare the provider identity with the pinned UID. Replacing Secret data with a kubeconfig for a different cluster produces `ClusterIdentityChanged`, it must not silently move existing objects to a new provider. Use a new Connection and a deliberate unbind/rebind workflow for a new provider identity.
+
+**Credential rotation limitation:** the Connection rereads Secret data, but an already-engaged instance-sync client is not rebuilt merely because a valid Secret changed while the Connection stayed Ready. After rotating credentials for the same provider, restart the konnector (or its deployment) and verify instance sync before revoking the old credential. The core does not renew tokens itself.
+
+The engine adds `core.kbind.io/cleanup` to the Connection and its referenced Secret. The Secret finalizer keeps credentials available during teardown, the engine does not delete the Secret for you.
+
+### Provider export boundary
+
+With the CRD source, label an API on the provider:
+
+```bash
+kubectl --context=provider label crd widgets.example.org \
+ core.kbind.io/exported=true --overwrite
+```
+
+Only the exact value `"true"` matches. Discovery normally refreshes every 30 seconds and appears in `Connection.status.exportedAPIs`.
+
+This is an opt-in discovery filter, not an RBAC mechanism. A user with CRD read permission can still read unlabelled CRDs outside kbind. The konnector also needs independent instance permissions.
+
+With OpenAPI, there is no exported-label check. The discovery/API boundary exposed by the provider credentials determines candidates, built-in Kubernetes groups are excluded. Not all discovery responses are filtered by object-level RBAC, so a discovered API is not proof that its instances can be read or written.
+
+### Schema source and policies
+
+Defaults are `source: Auto`, `pullPolicy: Bound`, `updatePolicy: Always`, and `autoBind: false`.
+
+#### Source
+
+| Source | Current behavior |
+| --- | --- |
+| `CRD` | List provider CRDs labelled `core.kbind.io/exported: "true"` and read selected CRD definitions. Recommended for an explicit opt-in on ordinary Kubernetes. |
+| `OpenAPI` | Discover non-built-in APIs and synthesize consumer CRDs using `/openapi/v3`. Useful for CRD-less providers. No CRD label gate. |
+| `Auto` | Try labelled CRD discovery first. A successful list with exports selects CRD, a successful list with no exports falls through to OpenAPI. |
+
+**Auto is not a fallback for a failed CRD list.** The current code returns list errors, including `Forbidden`, instead of falling back. Choose `OpenAPI` explicitly when CRDs cannot be listed. Conversely, use `CRD` explicitly when an empty label selection should export nothing: Auto can otherwise discover unlabelled APIs through OpenAPI.
+
+Inspect `status.activeSchemaSource` rather than assuming which source Auto selected.
+
+#### Which schemas are installed?
+
+| `pullPolicy` | Effect |
+| --- | --- |
+| `Bound` | Install only APIs named by a Binding or ClusterBinding. |
+| `All` | Install all discovered exported APIs, even without bindings. |
+| `None` | Do not create missing consumer CRDs, an external manager supplies them. |
+
+Installing a schema does not activate instance sync. A Binding/ClusterBinding is still required unless `autoBind` supplies one.
+
+With `None` and the **CRD** source, the engine still reads the provider CRD and stamps existing consumer CRDs with its managed and Connection markers. With the **OpenAPI** source, existing CRDs are only read: you must supply the markers yourself for the sync controller to discover them:
+
+```yaml
+metadata:
+ labels:
+ core.kbind.io/managed: "true"
+ annotations:
+ core.kbind.io/connection: provider
+```
+
+`None` does **not** protect a manually supplied CRD against ClusterBinding cleanup. See [GitOps](gitops.md) before sharing CRD ownership.
+
+#### Schema updates and fidelity
+
+`updatePolicy: Always` follows provider CRD spec changes for the CRD source. `Once` prevents subsequent spec updates. Neither is a compatibility check or a storage migration strategy.
+
+Current limitations:
+
+- A CRD-source pull keeps **one version**: the provider storage version, or the first served version if no storage version is found. It serves that version locally and forces conversion to `None`, removing the conversion webhook configuration.
+- Validation, defaults, and subresources present in that selected CRD version can be retained, but provider admission webhooks and controllers are not installed. Local acceptance does not guarantee provider acceptance.
+- OpenAPI synthesis can expose multiple discovered versions, with the discovery-preferred version selected for storage. It installs no conversion webhook, this does not reproduce provider-side multi-version conversion.
+- OpenAPI `$ref` schemas are replaced with permissive object schemas preserving unknown fields, not fully resolved. CEL/defaulting fidelity and provider admission behavior are not guaranteed. Short names, printer columns, and scale subresources are not reconstructed, discovered status subresources are.
+- With **OpenAPI + Bound**, the binding only synthesizes when the consumer CRD is absent. `Always` currently does not refresh an existing bound-only OpenAPI CRD. The `All` path does reconcile synthesized schemas repeatedly.
+- `Once` does not stamp an arbitrary existing CRD into management. Use an intentional externally managed workflow rather than assuming any preinstalled schema is adopted.
+
+Review consumer CRDs and run representative API validation/sync tests before exposing a provider API to applications. The provider remains authoritative for execution and admission.
+
+## ClusterBinding and Binding
+
+**Purpose**: Activate synchronization for the selected APIs. **Used by**: Service consumers. **Scope**: Cluster-wide (`ClusterBinding`) or one namespace (`Binding`).
+
+### Namespace selection and overlapping bindings
+
+A ClusterBinding covers all namespaces for a namespaced API, or the entire API for a cluster-scoped resource. A Binding covers only its own namespace and cannot activate cluster-scoped instance sync. There is no namespace label selector, namespace allowlist field, per-instance selector, or runtime rename/scope-conversion policy in the core.
+
+The stock mapper preserves namespace and name. `team-a/my-widget` on the consumer becomes `team-a/my-widget` on the provider. A provider kubeconfig's default namespace controls the heartbeat location, **not** this mapping.
+
+The CRD remains cluster-scoped even when a Binding is namespaced. Users in other namespaces can see/use that API if their local RBAC permits it, but their instances do not sync without a covering binding. Informers still read cluster-wide.
+
+Avoid overlapping bindings for an API. The resolver checks ClusterBindings before namespaced Bindings, including when the ClusterBinding is not Ready. It returns the first match at a given scope, there is no supported priority or merge policy among peers. A managed consumer CRD pins one Connection for its syncer, so binding the same API to different providers is not supported routing.
+
+### Automatic binding
+
+`spec.autoBind: true` makes the engine maintain a ClusterBinding **named after the Connection**, listing every exported API and owned by that Connection. Newly discovered exports can therefore start syncing without another user action. Reserve that name, do not also manage it as an independent GitOps object.
+
+When there are no exports, the reconciler deletes that ClusterBinding. Changing `autoBind` to `false` stops maintaining it but currently does **not** remove an already-created binding. Delete that binding deliberately if the goal is to stop syncing.
+
+## Export, Collection, and Grant
+
+The optional service layer uses `Export` to describe an offering, `Collection` to group offerings, and `Grant` to record issued credentials. They are cluster-scoped resources on the provider, in the `catalog.kbind.io` and `iam.kbind.io` groups. See [Catalog and Grants](catalog.md) for their structures.
+
+## Complete Binding Flow
+
+1. **Provider definition**: expose service APIs and credential RBAC, optionally with a catalog Export.
+2. **Consumer request**: bind an Export through the optional CLI/backend, or prepare core manifests directly.
+3. **Provider processing**: the optional issuer provisions a Grant's credentials.
+4. **Consumer binding**: apply the Secret, Connection, and bindings with the konnector installed.
+5. **Bidirectional sync**: consumer spec flows up and provider status flows down.
+
+### Observing progress
+
+Connection status reports identities, active schema source, exports, and conditions such as `SecretValid`, `Connected`, `PermissionDenied`, and `Ready`. Although `SchemaInSync` is defined in the API, the current Connection reconciler does not populate it.
+
+Binding status reports `boundAPIs`, schema hashes, conflict counts, and conditions including `Ready`, `Synced`, `Conflicts`, and `PermissionDenied`. `Synced=True` is a setup-level result, not a per-object delivery acknowledgement. Conflicting objects can coexist with a ready binding, some instance RBAC errors appear only as Events/logs.
+
+## Related Documentation
+
+Use [Synchronization](synchronization.md) for the data path and lifecycle, and [Troubleshooting](troubleshooting.md) to inspect actual object delivery.
diff --git a/docs/content/usage/catalog.md b/docs/content/usage/catalog.md
new file mode 100644
index 000000000..ae72e69ec
--- /dev/null
+++ b/docs/content/usage/catalog.md
@@ -0,0 +1,321 @@
+# Catalog, issuance, and inventories
+
+The optional backend turns exported provider APIs into a browsable catalog and issues the credentials needed to bind them. All service-layer resources below live on the **provider** and are cluster-scoped:
+
+| Resource | API | Purpose |
+| --- | --- | --- |
+| `Export` | `catalog.kbind.io/v1alpha1` | Describe an offering, its APIs, and binding defaults |
+| `Collection` | `catalog.kbind.io/v1alpha1` | Group Exports for browsing |
+| `Grant` | `iam.kbind.io/v1alpha1` | Record an identity's issued API access and credential artifacts |
+
+These are not consumer sync instructions. The konnector consumes the core Secret, Connection, and binding objects produced at the end of issuance. See [API concepts](api-concepts.md), the [catalog API reference](../reference/crd/index.md#catalogkbindiov1alpha1), and the [IAM API reference](../reference/crd/index.md#iamkbindiov1alpha1).
+
+## Publish an offering
+
+Start with a running [backend](../developers/backend/index.md) and a provider API that actually implements your service. In this checkout the sample API is **`widgets.example.org`**, not `widgets.example.com`.
+
+From the repository root:
+
+```sh
+kubectl --context=provider apply -f config/samples/provider-widget-crd.yaml
+```
+
+The sample CRD already has `core.kbind.io/exported: "true"`. For an existing CRD:
+
+```sh
+kubectl --context=provider label crd widgets.example.org \
+ core.kbind.io/exported=true --overwrite
+```
+
+The label is the core's export declaration. Creating a catalog Export does not label a CRD, install it, or start a controller that implements it.
+
+Save the following as `widgets-catalog.yaml`:
+
+```yaml
+apiVersion: catalog.kbind.io/v1alpha1
+kind: Export
+metadata:
+ name: widgets
+spec:
+ title: Widgets
+ description: A demonstration API for managed widgets.
+ docs: https://example.org/widgets
+ apis:
+ - name: widgets.example.org
+ defaults:
+ conflictPolicy: Fail
+ relatedResources:
+ - group: ""
+ resource: configmaps
+ direction: FromProvider
+ selector:
+ labelSelector:
+ matchLabels:
+ widgets.example.org/shared: "true"
+---
+apiVersion: catalog.kbind.io/v1alpha1
+kind: Collection
+metadata:
+ name: examples
+spec:
+ title: Example services
+ description: APIs for learning the binding workflow.
+ exports:
+ - name: widgets
+```
+
+```sh
+kubectl --context=provider apply -f widgets-catalog.yaml
+kubectl --context=provider wait --for=condition=Ready \
+ exports.catalog.kbind.io/widgets --timeout=60s
+kubectl --context=provider get exports.catalog.kbind.io,collections.catalog.kbind.io
+```
+
+The issuer module's Export controller verifies that **every named CRD** exists with the export label. Missing or unexported APIs produce `Ready=False` with reason `APINotExported`. The gateway hides non-ready Exports and refuses to bind them. This readiness condition does not check service-controller health, API-specific business logic, or an individual consumer's connectivity.
+
+Collections are presentation only. Their `exports` entries refer to Export names, not CRD names. They cannot be bound as a unit, and do not grant access. The HTTP catalog includes only their ready members, and omits Collections with no visible members. There is no Collection readiness controller.
+
+### Related resources
+
+The example offering declares provider-to-consumer ConfigMap sync. Create an object that matches its label:
+
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: widget-demo
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: widget-settings
+ namespace: widget-demo
+ labels:
+ widgets.example.org/shared: "true"
+data:
+ endpoint: https://widgets.example.org
+```
+
+Apply this YAML on the provider. With the default cluster-wide bundle and compatible credentials, the related ConfigMap keeps its name and namespace on the consumer. It is not a JSONPath-followed reference from a Widget.
+
+Rules support only `secrets` and `configmaps` in the core API group, with `FromProvider` or `FromConsumer` direction. Select by Kubernetes `labelSelector`, exact `names`, or both. There is no reference-following selector. An omitted selector is broad, use explicit selectors, especially for Secrets.
+
+For example, an additional rule can send selected consumer credentials to the provider:
+
+```yaml
+group: ""
+resource: secrets
+direction: FromConsumer
+selector:
+ labelSelector:
+ matchLabels:
+ widgets.example.org/input: "true"
+ names:
+ - widget-input
+```
+
+This fragment belongs under `spec.defaults.relatedResources`. It is an explicit authorization and data-flow decision, not an automatic dependency discovery feature. Do not add it unless those consumer Secrets should leave their cluster.
+
+Related-resource selectors control sync, not issued RBAC. In default `issuerScope: Cluster`, a `FromProvider` Secret rule grants Secret reads across provider namespaces, not only the selected Secrets. A `FromConsumer` rule adds write permissions too. Review [credential scope](../developers/backend/index.md#credential-scope-and-installer-privileges) before publishing secret-bearing offerings.
+
+### How defaults propagate
+
+The gateway copies an Export's `spec.apis`, `spec.defaults.conflictPolicy`, and `spec.defaults.relatedResources` into the Grant. Bundle assembly copies those fields from the Grant into a generated **`ClusterBinding`**.
+
+The Grant is a resolved record, not a live reference to Export defaults. Editing an Export alone does not change existing Grants or consumer bindings. Binding the same Export again as the same identity updates that Grant's spec from the current Export and returns a new bundle. Apply that bundle to update the consumer.
+
+`Fail` is the core's default conflict policy. `Adopt` can take over an unowned object, but does not authorize stealing another binding's managed object. The catalog has no defaults for namespace remapping, arbitrary object templates, or a namespaced `Binding` output, those are not gateway bind options.
+
+## Browse and bind
+
+Using the [v2 CLI](../setup/kubectl-plugin.md):
+
+```sh
+kubectl bind login https://bind.example.com
+kubectl bind catalog
+kubectl bind export widgets \
+ --kubeconfig=./consumer.kubeconfig \
+ --install-konnector=false
+```
+
+This example assumes the konnector and core CRDs are already installed. `export` otherwise defaults to installing/upgrading the konnector, choose a matching image explicitly if you use that behavior.
+
+In the browser:
+
+1. Open the gateway root, sign in, and choose **Catalog**.
+2. Inspect the offering's API names, description, documentation link, and related-resource direction/selector hints.
+3. Click **Bind**. The gateway provisions a Grant and opens a one-time bundle dialog.
+4. Choose **Download bundle** or use the displayed `curl | kubectl apply` command once. Downloading and then attempting the same pickup URL again will fail, apply the downloaded file instead.
+
+Install the konnector before applying a downloaded bundle. **Clusters → Connect a cluster** provides the install command and manifest download. This does not establish a provider Connection by itself.
+
+The dialog also has **Copy kbind command**. That command requires its own CLI login and makes a fresh bind request, it does not redeem the dialog's existing ticket. If browser apply is enabled by the operator, the UI additionally allows you to paste a consumer kubeconfig for installation or bind-and-apply. This sends consumer credentials through the gateway. Prefer local CLI or reviewed manifest apply when that trust is not appropriate.
+
+The browser does not edit Exports/Collections, delete Grants, choose an existing cluster as an apply target, or create service instances. Use Kubernetes tools for those operations. An inventory entry is not a stored consumer credential.
+
+### CLI sessions and cluster selection
+
+`kubectl bind login` opens the gateway's OIDC login URL and listens for the browser's callback on a random localhost port. Login times out after five minutes. `--no-browser` prints the URL instead, but the browser must still reach that localhost listener. It is not a device-code login.
+
+The last successful login selects the default gateway. Override it with `kubectl bind catalog --server=https://other-bind.example.com`. The URL is a gateway, not a Kubernetes API server. Use an explicit `http://` scheme for local development.
+
+The CLI keeps bearer credentials in `kbind/config.json` under Go's `os.UserConfigDir()`:
+
+| Platform | Usual location |
+| --- | --- |
+| Linux | `$XDG_CONFIG_HOME/kbind/config.json`, or `~/.config/kbind/config.json` |
+| macOS | `~/Library/Application Support/kbind/config.json` |
+| Windows | `%AppData%\kbind\config.json` |
+
+New files use mode `0600` where supported. This is not an encrypted credential vault, protect existing file permissions too. There is no CLI logout command. Removing the cached token forgets it locally but does not revoke credentials, and browser sign-out does not invalidate a copied session token. The CLI has no Kubernetes-token login mode, though the backend can support [TokenReview authentication](../developers/backend/index.md#kubernetes-tokenreview-authentication).
+
+Consumer selection follows client-go loading: `--kubeconfig`, then `KUBECONFIG`, then the normal kubeconfig and its current context. There are no `--context` or `--namespace` overrides. Use a dedicated consumer kubeconfig when managing several clusters.
+
+### Installing through the CLI
+
+To install without Helm, choose a built v2 image available to your cluster:
+
+```sh
+kubectl bind connect --kubeconfig=./consumer.kubeconfig \
+ --konnector-image=registry.example.com/platform/konnector:v2-example
+kubectl --kubeconfig=./consumer.kubeconfig -n kbind \
+ rollout status deployment/konnector
+```
+
+Replace the illustrative image. The compiled `latest` default is not a v2 guarantee. `connect` installs locally embedded manifests without contacting the gateway or requiring login. It has no print-only or dry-run mode. It does not register a consumer in the provider inventory, that requires a bound Connection to start heartbeating.
+
+`export` also installs by default. Use `--install-konnector=false` for an existing Helm/GitOps-managed konnector, or pass `--konnector-image` to select the image explicitly. Installation and bundle apply use server-side apply with forced field ownership and can overwrite another manager's fields. They are not transactional, failure can leave earlier objects installed. Review the [installer permissions](../developers/konnector/index.md#rbac-and-credentials).
+
+The public gateway endpoint `/api/konnector` serves a separate installation manifest using the backend's `konnectorImage` setting. Download and inspect it before applying. Its image can differ from the CLI's.
+
+## Binding and bundle pickup
+
+The gateway flow is:
+
+1. Authenticate the caller and require a ready Export.
+2. Create or update one Grant for `(Export name, identity subject)`.
+3. Wait up to 30 seconds for the issuer's `Ready=True`.
+4. Return a one-time pickup URL and its expiration.
+5. Redeem that URL for the consumer bundle.
+
+There are no bind-request CRDs, request phases, approval queues, or browser device-code polling in this protocol.
+
+The Grant name is `-`, where the hash is the first ten hexadecimal characters of SHA-256 of the identity subject. The provider boundary namespace is `kbind-` and is shared by the identity's Grants. OIDC subjects are `#`, TokenReview subjects are `kubernetes#`.
+
+The issuer creates a ServiceAccount and token Secret per Grant, resource-specific RBAC, and a Lease permission home in the boundary namespace. The token Secret is populated by Kubernetes' ServiceAccount token controller. `Grant.status` reports `namespace`, `serviceAccount`, `tokenSecret`, and conditions.
+
+The bundle always contains:
+
+| Object | Name / namespace | Purpose |
+| --- | --- | --- |
+| `v1/Secret` | Grant name, namespace `kbind` | `stringData.kubeconfig` with the provider token, endpoint, CA, and boundary context namespace |
+| `core.kbind.io/v1alpha1/Connection` | Grant name, cluster-scoped | Reference the kubeconfig Secret |
+| `core.kbind.io/v1alpha1/ClusterBinding` | Grant name, cluster-scoped | Select the Grant's APIs and resolved defaults |
+
+It does not include the core CRDs, konnector Deployment, `kbind` namespace, or service instances. CLI install/apply can supply installation and namespace prerequisites, plain `kubectl apply` of the bundle cannot.
+
+### Three different lifetimes
+
+| Item | Default lifetime | What expiry/revocation means |
+| --- | --- | --- |
+| Browser/CLI session | 12 hours | Log in again to call authenticated gateway APIs |
+| Pickup URL | 5 minutes, one successful consumption | Bind again for a new pickup URL |
+| Issued provider credential | Long-lived Secret-based SA token | Remains usable independently of session/pickup expiry until revoked or otherwise invalidated |
+
+The one-time token is stored on the Grant as a hash and expiry, not gateway memory. Optimistic concurrency enforces single consumption across replicas. A new bind replaces any outstanding pickup for the same Grant. A used, expired, or unknown token returns HTTP `410`.
+
+Pickup consumes the token **before** assembling the bundle. If the response fails, is lost, or credential assembly fails after consumption, bind again, there is no retryable download ticket. Protect pickup URLs like passwords: possession is sufficient to retrieve the credentials without a session.
+
+`kubectl bind export widgets -o yaml` is the GitOps path. It still provisions and picks up a real Grant, but does not apply to the consumer. Secure the credential material before storing it in Git. See [GitOps](gitops.md).
+
+### Direct Grant management
+
+Normally the gateway creates Grants. Trusted provider automation can also create them through Kubernetes:
+
+```yaml
+apiVersion: iam.kbind.io/v1alpha1
+kind: Grant
+metadata:
+ name: widgets-automation
+spec:
+ identity:
+ subject: https://sso.example.com#automation
+ displayName: Widget automation
+ exportName: widgets
+ apis:
+ - name: widgets.example.org
+ conflictPolicy: Fail
+ relatedResources:
+ - group: ""
+ resource: configmaps
+ direction: FromProvider
+ selector:
+ labelSelector:
+ matchLabels:
+ widgets.example.org/shared: "true"
+```
+
+The issuer provisions from this spec. It does not resolve `exportName` back to an Export, validate caller entitlement, or mint a pickup URL simply because a Grant exists. Direct Grant writers are trusted credential administrators. The illustrative name above is not the gateway's deterministic name, binding through the gateway would use its own record. For a complete gateway-issued bundle, use the bind API rather than hand-authoring Grant status or pickup annotations.
+
+## Revoke and retire an offering
+
+Inspect provider records:
+
+```sh
+kubectl --context=provider get grants.iam.kbind.io
+kubectl --context=provider get grants.iam.kbind.io GRANT_NAME -o yaml
+```
+
+Revoke a specific Grant:
+
+```sh
+kubectl --context=provider delete grants.iam.kbind.io GRANT_NAME
+```
+
+The issuer's `iam.kbind.io/cleanup` finalizer removes that Grant's ServiceAccount, token Secret, ClusterRole/ClusterRoleBinding, and Role/RoleBinding. The issuer must be running for finalizer cleanup to complete. Do not remove the finalizer as a substitute for credential revocation.
+
+Ordinary revocation keeps the provider boundary namespace and synced objects. It also does not remove consumer Secrets, Connections, bindings, or CRDs. Consumers should deliberately disengage through their [core lifecycle](api-concepts.md) rather than assume provider credential revocation cleaned up their cluster.
+
+Deleting an Export removes it from catalog/bind lookup but does not delete its existing Grants or revoke their tokens. Removing a CRD's export label makes the catalog Export non-ready after reconciliation, it is not a credential revocation operation either. Revoke outstanding Grants explicitly when retiring access.
+
+Deleting a Grant is not a permanent deny-list entry: an authenticated caller can bind an offering again while it remains available. Plan gateway access and offering withdrawal as well as credential cleanup.
+
+### Reaper policy
+
+The reaper is disabled by default. With it enabled:
+
+- `reaper.ttl` / `--reaper-ttl` defaults to **30 minutes**.
+- `reaper.interval` / `--reaper-interval` defaults to **1 minute**.
+- `reaper.revoke` defaults to `false`: mark `Stale=True` and add `iam.kbind.io/stale-since`, but retain credentials.
+- `reaper.deleteBoundary` defaults to `false` and only has an effect when revocation is enabled.
+
+It reads managed heartbeat Leases in the Grant's boundary namespace. Matching uses the Lease's `core.kbind.io/connection` annotation and the generated Connection/Grant name. If matching Leases exist, the newest matching renewal determines staleness (`LeaseExpired`). Without a matching Lease, another fresh Lease in the same boundary conservatively keeps the Grant alive, accommodating renamed Connections. A fully silent boundary becomes stale. If no renewed Lease exists, the reaper waits one TTL after the Grant's Ready transition (`NoLease`). Fresh heartbeats clear prior stale markings.
+
+With revocation enabled, the reaper deletes stale Grants and the issuer finalizer revokes their credentials. With boundary deletion also enabled, it can delete the namespace and everything in it, but skips deletion while another non-deleting Grant shares that identity.
+
+This is **not** complete cleanup of a consumer's synced objects. Default cluster-scoped credentials reproduce consumer namespace names, many provider objects can therefore live outside the credential boundary. Boundary deletion does not delete those namespaces or cluster-scoped objects.
+
+Coordinate destructive policy with outages, paused consumers, and GitOps rollouts. A downloaded-but-not-applied bundle may be revoked as never connected. The reaper's TTL is independent of the inventory's Live/Stale display threshold.
+
+## Cluster and instance inventories
+
+The browser's **Clusters** view and `kubectl bind clusters` use `GET /api/clusters`. There is no cluster registration resource or browser `/clusters` page route, the browser switches views within the app.
+
+The inventory correlates Grants with managed Leases using the Connection name and consumer cluster UID annotations. It reports:
+
+- Consumer UID, latest heartbeat, and whether any binding is live.
+- Per-binding Grant, Export, identity, boundary namespace, and heartbeat.
+- Best-effort counts of managed provider objects per API and consumer UID.
+- Pending Grants without a usable matching Lease (`NeverConnected` in the CLI).
+
+“Live” means a renewal within twice `leaseDurationSeconds`, a missing duration uses 60 seconds, hence a 120-second grace window. It is not a guarantee that every resource is syncing correctly. “NeverConnected” means the inventory found no matching Lease, not proof the bundle was never applied: renamed Connections, deleted Leases, or missing annotations can also prevent correlation.
+
+`GET /api/catalog/{export}/instances`, the browser's **Synced instances** expander, and `kubectl bind instances [export]` show **provider-side** object metadata, not service health or a remote consumer object browser:
+
+- Bound-API rows are objects carrying the core managed label, with consumer UID and identity attribution when the Lease/Grant link is available.
+- Related `FromConsumer` rows are managed provider copies marked as related resources, the listing applies name restrictions, but is not a strict per-Grant or label-selector-filtered inventory.
+- Related `FromProvider` rows are provider originals matching the declared label selector (and names if supplied). Without a `labelSelector`, these originals are omitted even if a names-only sync rule is valid. Consumer-side copies are not inspected. Secret payloads are not returned by this endpoint.
+
+Counts and instance lists are best effort. Unreadable/unavailable APIs can yield zero counts or omitted rows instead of failing the entire view. Multiple Exports sharing an API can show the same objects, the inventory is not accounting by individual Grant. It also follows the current Export definition, not every historical Grant's defaults.
+
+All authenticated gateway callers can read these inventories, they are not filtered to the caller's identity. If results are unexpected, inspect provider Grants and Leases and follow [troubleshooting](troubleshooting.md).
diff --git a/docs/content/usage/gitops.md b/docs/content/usage/gitops.md
new file mode 100644
index 000000000..bded92691
--- /dev/null
+++ b/docs/content/usage/gitops.md
@@ -0,0 +1,132 @@
+# GitOps
+
+The v2 core accepts ordinary Kubernetes YAML. You can manage a Connection and its explicit bindings with your existing GitOps controller, no browser onboarding or continuously running CLI is required.
+
+## Separate definitions, credentials, and generated state
+
+Manage these layers deliberately:
+
+1. Install the three core CRDs and the konnector.
+2. Create application/credential namespaces and deliver provider credentials securely.
+3. Apply Connections and explicit Bindings/ClusterBindings.
+4. Wait for the bound consumer CRDs to become Established before applying their instances.
+
+Connections and bindings tolerate arriving before their dependencies, but Kubernetes rejects unknown kinds before a CRD exists. Use your GitOps tool's existing dependency/wave/health mechanisms rather than relying on YAML document order.
+
+Keep these out of Git:
+
+- Plaintext provider kubeconfigs and base64-only Secret manifests containing credentials.
+- Controller-written `status`, UIDs, resource versions, finalizers, and managed fields.
+- Generated autoBind ClusterBindings, unless you instead turn autoBind off and manage explicit bindings.
+- Consumer copies of `FromProvider` related resources.
+
+Use an existing encrypted-Secret or external-secret workflow for credentials. The resulting Kubernetes Secret must have the namespace, name, and data key referenced by the Connection.
+
+## Obtain a bundle from a provider
+
+With the [CLI](../setup/kubectl-plugin.md) installed and logged in:
+
+```sh
+kubectl bind export widgets -o yaml > widgets-bundle.yaml
+```
+
+This provisions a real Grant and redeems a one-time pickup URL, it is not a dry run. YAML is the only supported output format. This mode does not read the consumer kubeconfig, install the konnector, create namespaces, or apply resources, even when `--install-konnector` is true.
+
+Install the core CRDs, konnector, and `kbind` namespace separately. The bundle contains a long-lived provider token in plaintext YAML. Encrypt its Secret or use a secret manager before committing anything.
+
+Pickup expiry does not expire a saved bundle's credentials. The provider's optional reaper can revoke Grants that have not yet heartbeated, so coordinate its TTL with rollout timing. See [bundle lifetimes](catalog.md#three-different-lifetimes).
+
+## An explicit application binding
+
+After creating `team-a` and delivering `kbind/provider-kubeconfig`:
+
+```yaml
+apiVersion: core.kbind.io/v1alpha1
+kind: Connection
+metadata:
+ name: provider
+spec:
+ kubeconfigSecretRef:
+ namespace: kbind
+ name: provider-kubeconfig
+ key: kubeconfig
+ schema:
+ source: CRD
+ pullPolicy: Bound
+ updatePolicy: Always
+ autoBind: false
+---
+apiVersion: core.kbind.io/v1alpha1
+kind: Binding
+metadata:
+ name: widgets
+ namespace: team-a
+spec:
+ connectionRef:
+ name: provider
+ apis:
+ - name: widgets.example.org
+ conflictPolicy: Fail
+```
+
+A namespaced Binding is often preferable when only one application's namespace should sync. It is not a namespaced CRD or a replacement for provider/consumer RBAC.
+
+Use an explicit API allowlist rather than `autoBind: true` if newly exported provider APIs require review. Use `source: CRD` when provider labels must remain the opt-in boundary, Auto can fall through to OpenAPI when no labelled CRDs are found.
+
+## Choose one schema owner
+
+### Konnector-managed schemas
+
+With `pullPolicy: Bound` or `All`, avoid also reconciling the same consumer CRD spec from Git. `updatePolicy: Always` can overwrite competing changes with the provider schema.
+
+`Once` stops subsequent schema spec updates, but is not a version selector or approval queue. Test compatibility before changing policies, and note the [OpenAPI update limitation](api-concepts.md#schema-updates-and-fidelity).
+
+### Externally managed schemas
+
+Use `pullPolicy: None` when Git owns the consumer CRD spec. Supply the correct group, resource, scope, usable storage version, status schema/subresource, and compatible validation yourself.
+
+For OpenAPI-source Connections, include these markers in the consumer CRD:
+
+```yaml
+metadata:
+ labels:
+ core.kbind.io/managed: "true"
+ annotations:
+ core.kbind.io/connection: provider
+```
+
+For CRD-source Connections, the engine stamps these markers on an existing CRD. Configure your GitOps tool not to fight those metadata changes.
+
+**Important:** `None` means “do not install the schema,” not “never manage or delete this CRD.” Last-ClusterBinding cleanup currently deletes the referenced CRD even under `None`. Prefer a carefully controlled Binding lifecycle, disable destructive automatic pruning for shared resources, and read [unbinding behavior](synchronization.md#delete-a-binding).
+
+Likewise, core CRDs included by the Helm chart are ordinary release templates. If Git owns those three definitions, use `installCRDs: false` from the initial Helm install, do not hand their lifecycle back and forth casually.
+
+## Drift and ownership
+
+For bound instances, Git should normally manage consumer `spec`, not provider copies or consumer `status`. The konnector force-applies its fields to owned provider copies, so a second provider-side GitOps manager for those same fields will compete with it.
+
+An existing foreign provider object is not adopted by default. `conflictPolicy: Adopt` is an explicit and potentially destructive decision for a markerless object, not a generic way to suppress drift. It does not steal objects marked for another consumer.
+
+Avoid managing the same API through multiple overlapping bindings or different Connections. The current resolver is not a multi-provider routing system. Also avoid editing a binding's API list or Connection reference as though it performed an atomic migration: cleanup operates on deletion and the current spec, not an inventory of all previous specs.
+
+## Rotate without replacing identity
+
+Keep `kubeconfigSecretRef` stable and update the Secret's data for the same provider cluster. The reference and pinned identities are immutable.
+
+After delivering new valid credentials, restart the konnector to rebuild engaged instance clients, then verify spec/status round-tripping before revoking the old credentials. Secret updates alone do not guarantee hot rotation of an already-engaged client.
+
+A different provider identity requires a new Connection and an explicit migration. Do not try to overwrite `remoteClusterUID`.
+
+## Pruning is unbinding
+
+Removing a binding from Git and allowing it to be pruned invokes the same destructive cleanup as `kubectl delete`. Removing a last ClusterBinding can delete the consumer CRD and every instance of that API.
+
+For a planned removal:
+
+1. Pause automatic recreation/pruning while coordinating the change.
+2. Back up relevant consumer resources and annotate instances with `core.kbind.io/deletion-policy: Orphan` if provider copies must remain.
+3. Delete and verify instances or bindings while the controller and credentials work.
+4. Delete Connections after explicit bindings have drained.
+5. Remove credential Secrets, then the controller/CRDs if no longer needed.
+
+Do not prune the credentials, namespace, CRDs, and controller first. Cleanup finalizers preserve ordering in common cases, but they are not a distributed rollback mechanism. See [Troubleshooting](troubleshooting.md) for incomplete teardown.
diff --git a/docs/content/usage/index.md b/docs/content/usage/index.md
new file mode 100644
index 000000000..9b3d0bf48
--- /dev/null
+++ b/docs/content/usage/index.md
@@ -0,0 +1,93 @@
+---
+title: Usage Guide
+description: |
+ Comprehensive guide to using kube-bind APIs, concepts, and workflows for service providers and consumers.
+weight: 200
+---
+
+# kube-bind Usage Guide
+
+This section provides comprehensive documentation on how to use kube-bind's core APIs and concepts. Whether you're a service provider looking to export APIs or a consumer wanting to bind to services, this guide covers the essential workflows and components.
+
+## Core Concepts
+
+kube-bind operates on three fundamental concepts:
+
+### Service Provider
+
+The cluster that **exports** APIs and resources, making them available for other clusters to consume. Service providers label exported CRDs, grant credential RBAC, and optionally publish catalog Exports.
+
+### Service Consumer
+
+The cluster that **imports** and uses APIs from service providers. Consumers apply a Connection and bindings directly, or obtain a bundle through the optional backend.
+
+### Konnector Agent
+
+The component that establishes and maintains the secure connection between provider and consumer clusters, synchronizing resources and handling permissions.
+
+## Key API Types
+
+### Connection
+
+**Purpose**: References provider credentials, discovers APIs, and controls schema delivery. **Used by**: Service consumers **Scope**: Cluster-scoped on the consumer
+
+### ClusterBinding and Binding
+
+**Purpose**: Select the APIs whose instances should synchronize. **Used by**: Service consumers **Scope**: Cluster-wide or one consumer namespace
+
+### Export and Collection
+
+**Purpose**: Describe and group offerings in the optional catalog. **Used by**: Service providers **Scope**: Cluster-scoped on the provider
+
+### Grant
+
+**Purpose**: Record issued credentials and resolved APIs. **Used by**: The optional backend's gateway and issuer **Scope**: Cluster-scoped on the provider
+
+## Documentation Structure
+
+### [API Concepts](api-concepts.md)
+
+Deep dive into the core API types, their relationships, and how they work together in the kube-bind ecosystem.
+
+### [Catalog and Grants](catalog.md)
+
+Publish offerings, issue credentials, and inspect connected consumers.
+
+## Common Workflows
+
+### For Service Providers
+
+1. **Export APIs** with CRD labels and RBAC, optionally describing them as catalog Exports.
+2. **Implement service** to act on the synced/bound objects so it can be returned to the consumer/user.
+
+### For Service Consumers
+
+1. **Authenticate** to the kube-bind backend
+2. **Discover available Exports** through the web UI or CLI
+3. **Request a bundle** for a specific Export and apply it to the consumer
+4. **Use imported APIs** in their local cluster
+
+The core-only alternative is to supply credentials and bindings directly through [GitOps](gitops.md), with no backend login.
+
+### For Platform Operators
+
+1. **Deploy the konnector** on the consumer and the optional backend on the provider
+2. **Configure authentication** and security policies
+3. **Monitor connections** and resource synchronization
+
+## Getting Started
+
+If you're new to kube-bind:
+
+1. **Start with the [Quickstart Guide](../setup/quickstart.md)** for a hands-on introduction
+2. **Review [API Concepts](api-concepts.md)** to understand the fundamental types
+3. **Check the [Reference Documentation](../reference/index.md)** for complete API specifications
+
+The konnector agents establish a secure, authenticated connection that allows:
+
+- **API schema synchronization** from provider to consumer
+- **Spec up / status down** resource data flow
+- **Selected Secret and ConfigMap synchronization**
+- **Provider access** governed by credential RBAC
+
+The stock syncer does not rename namespaces or convert resource scope. Review [synchronization](synchronization.md) when designing a shared provider.
diff --git a/docs/content/usage/integrations/.pages b/docs/content/usage/integrations/.pages
new file mode 100644
index 000000000..f62acf42b
--- /dev/null
+++ b/docs/content/usage/integrations/.pages
@@ -0,0 +1,6 @@
+nav:
+ - index.md
+ - cert-manager.md
+ - crossplane.md
+ - cloudnativepg.md
+ - kro.md
diff --git a/docs/content/usage/integrations/cert-manager.md b/docs/content/usage/integrations/cert-manager.md
new file mode 100644
index 000000000..2f0a66005
--- /dev/null
+++ b/docs/content/usage/integrations/cert-manager.md
@@ -0,0 +1,138 @@
+---
+title: Cert-Manager
+description: |
+ Guide on integrating kube-bind with cert-manager for automated TLS certificate management.
+weight: 10
+---
+
+# Cert-Manager Integration
+
+## Setup
+
+The following sections will guide you through the one-time setup that is required for providing certificates using cert-manager and kube-bind.
+
+Follow the [prerequisites](index.md#prerequisites), then select the provider:
+
+```bash
+kubectl config use-context provider
+```
+
+### Install cert-manager
+
+Install cert-manager in your Kubernetes cluster, where kube-bind backend is running, if you haven't already. You can follow the [official installation guide](https://cert-manager.io/docs/installation/kubernetes/).
+
+### Export the Certificate CRD
+
+To export the cert-manager `Certificate` CRD, add the kube-bind export label to it:
+
+```bash
+kubectl label crd certificates.cert-manager.io core.kbind.io/exported=true --overwrite
+```
+
+### Create a SelfSigned Issuer
+
+```bash
+kubectl apply -f - < 8888:80
+```
+
+Send the request through the gateway service to your backend application.
+
+```bash
+curl --verbose --header "Host: www.example.com" http://localhost:8888/headers
+```
+```text
+* Host localhost:8888 was resolved.
+* IPv6: ::1
+* IPv4: 127.0.0.1
+* Trying [::1]:8888...
+* Connected to localhost (::1) port 8888
+> GET /headers HTTP/1.1
+> Host: www.example.com
+> User-Agent: curl/8.7.1
+> Accept: */*
+>
+* Request completely sent off
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< date: Fri, 19 Dec 2025 19:50:35 GMT
+< content-length: 521
+< x-response-message: hello-kube-bind
+<
+{
+ "path": "/headers",
+ "host": "www.example.com",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "User-Agent": [
+ "curl/8.7.1"
+ ],
+ "X-Custom-Message": [
+ "hello-kube-bind"
+ ],
+ "X-Envoy-External-Address": [
+ "172.18.0.2"
+ ],
+ "X-Forwarded-For": [
+ "172.18.0.2"
+ ],
+ "X-Forwarded-Proto": [
+ "http"
+ ],
+ "X-Request-Id": [
+ "8c23f131-a328-485f-b7e1-2e6b20362af1"
+ ]
+ },
+ "namespace": "default",
+ "ingress": "",
+ "service": "",
+ "pod": "backend-77d4d5968-glxtp"
+}
+* Connection #0 to host localhost left intact
+```
diff --git a/docs/content/usage/migration.md b/docs/content/usage/migration.md
new file mode 100644
index 000000000..762dff395
--- /dev/null
+++ b/docs/content/usage/migration.md
@@ -0,0 +1,44 @@
+# Moving from 0.x
+
+v2 replaces the 0.x API model and binding protocol. **There is no automatic conversion or supported in-place upgrade of existing bindings.** Keep the [0.x documentation](https://docs.kbind.dev/latest/) for existing deployments, use separate test clusters to evaluate v2 first.
+
+## Concepts that carry over
+
+The consumer is still the source of desired state. The provider runs the service implementation. The konnector still sends spec to the provider and returns status to the consumer, connecting outward from the consumer.
+
+The API objects and operational responsibilities around that loop have changed:
+
+| 0.x concept | v2 replacement or change |
+| --- | --- |
+| `kube-bind.io` API group and its `v1alpha1`/`v1alpha2` kinds | `core.kbind.io/v1alpha1`, `catalog.kbind.io/v1alpha1`, and `iam.kbind.io/v1alpha1`, new objects, not a version-field edit |
+| Consumer `APIServiceBinding` and connection bookkeeping | `Connection` references credentials, `ClusterBinding` or `Binding` chooses APIs and sync scope |
+| `APIServiceExportTemplate` | Optional catalog `Export` |
+| Legacy `Collection` | New catalog `Collection` grouping Exports, create a new manifest |
+| `APIServiceExportRequest` / binding handshake | Optional HTTP `/api/bind` produces a one-apply core bundle |
+| Per-consumer `APIServiceExport` | Provider opt-in labels and core discovery, optional `Grant` records service-layer issuance |
+| `BoundSchema` / old schema intermediates | Core pulls CRDs or synthesizes them from OpenAPI, controlled by `Connection.spec.schema` |
+| `APIServiceNamespace` / mapped provider namespaces | No equivalent in stock core, namespace and name stay unchanged |
+| `Prefixed`, `Namespaced`, and `None` isolation modes | Identity mapping only in the shipped binary, no scope conversion |
+| Permission claims and JSONPath reference following | Credential RBAC plus explicit `relatedResources` selectors for Secrets/ConfigMaps |
+| Backend required for the binding protocol | Backend optional, directly apply the Secret, Connection, and bindings |
+
+The new `ClusterBinding` shares a name with an old kind, not its group, schema, or meaning. Use fully qualified resource names when inspecting clusters that contain both API groups.
+
+## Plan an explicit cutover
+
+1. Inventory the APIs, objects, credentials, namespace mapping, related Secrets/ConfigMaps, and cleanup policies in your current installation. Back up both clusters' relevant resources and service data.
+2. Choose a provider boundary that works with identity mapping. A 0.x object named with a consumer prefix or stored in a mapped namespace is not the same v2 target.
+3. Install v2 on an isolated consumer. Use the [core quickstart](../setup/quickstart.md) to establish the new workflow. Keep the old konnector away from APIs and objects managed by the new one.
+4. Recreate provider credentials and exported-API policy, then author new [core manifests](api-concepts.md). For the service layer, create [Exports and Collections](catalog.md) and obtain a new v2 bundle.
+5. Recreate representative instances and validate schema behavior, spec ownership, status delivery, related resources, RBAC, and cleanup before planning a service-specific data migration.
+6. Drain and retire old bindings using the cleanup behavior documented for their original release. Retain backups and credentials until the intended cleanup has completed.
+
+Do not run both syncers against the same target objects as a migration shortcut. Do not switch `conflictPolicy` to `Adopt` just to bypass an unexplained collision: v2 adoption can take over an unowned target, not a target marked as owned by a different binding.
+
+## Integration changes
+
+The old cert-manager, Crossplane, CloudNativePG, and kro recipes are not v2 installation instructions. Their service operators may still run on a provider, but each offering needs a new Export or explicit core binding, v2-compatible schemas, matching names and scopes, and sufficient RBAC.
+
+Review multi-version CRDs, conversion webhooks, built-in Kubernetes resource dependencies, and service-specific Secret references individually. v2's related-resource selectors do not follow arbitrary JSONPath references. The previous `Namespaced` isolation mode cannot be reproduced by changing a chart value or a `Mapper`.
+
+See [synchronization](synchronization.md) for the shipped semantics and [architecture](../developers/architecture.md) for extension points.
diff --git a/docs/content/usage/synchronization.md b/docs/content/usage/synchronization.md
new file mode 100644
index 000000000..e59c2155c
--- /dev/null
+++ b/docs/content/usage/synchronization.md
@@ -0,0 +1,192 @@
+---
+title: Resource Synchronization
+description: >
+ Resource synchronization, ownership, and lifecycle between consumer and provider clusters.
+weight: 220
+---
+
+# Resource Synchronization
+
+This section outlines how kbind synchronizes objects between two kinds of clusters:
+
+* The **provider cluster** is run by a service provider and hosts a service, operator, or other Kubernetes API. The optional backend offers these APIs through a catalog.
+* The **consumer clusters** are where end users consume services/APIs offered by providers.
+
+There is a 1:n relationship: one provider can serve many consumer clusters.
+
+## Overview
+
+The konnector turns managed consumer CRDs into per-resource sync controllers. A Ready Connection supplies the provider client, and a Ready Binding or ClusterBinding selects which consumer instances participate. See [API concepts](api-concepts.md) for scope, schema delivery, and overlapping-binding restrictions.
+
+### Sync Direction
+
+The consumer clusters represent the source of truth for desired state, and the provider holds copies of those objects. During synchronization, spec is copied from consumer to provider and status is copied in the opposite direction.
+
+| Data | Direction | Behavior |
+| --- | --- | --- |
+| Bound instance `spec` | Consumer → provider | Server-side apply using field manager `kbind-konnector`, with force ownership after the object-ownership check. |
+| Bound instance `status` | Provider → consumer | Copies the provider status through the consumer status subresource. |
+| Related Secret/ConfigMap payload | Configured per entry | Server-side apply using `kbind-konnector-related`. |
+
+Instance namespace/name are unchanged. The engine does not copy arbitrary consumer labels, annotations, owner references, or finalizers to the provider, it writes its own management and ownership metadata. Top-level fields other than `spec` are not a generic object replication interface.
+
+Status is copied when the provider object contains it. Absence of provider status does not actively clear an existing consumer status. APIs intended for status round-tripping need an appropriate status schema/subresource on the consumer.
+
+Consumer changes and provider watch events drive synchronization. Provider events also allow desired consumer specs to repair provider drift. There are periodic resyncs as a backstop, this is asynchronous reconciliation, not a cross-cluster transaction or a synchronous admission guarantee.
+
+### Connectivity
+
+The object synchronization logic lives in the konnector, an agent running on each consumer cluster. It reads a local Secret containing a kubeconfig for the provider. Connections and bindings in the consumer describe what to synchronize.
+
+This design allows consumer clusters to be mostly firewalled off, but requires provider API servers to be reachable from consumers. No inbound provider connection to the consumer is needed for core synchronization.
+
+### RBAC
+
+Provider credentials govern what the konnector may access. Object ownership checks do not protect against direct use of overprivileged credentials. A namespaced Binding restricts sync selection but does not make the current dynamic informers namespace-scoped. See [RBAC and credentials](../developers/konnector/index.md#rbac-and-credentials).
+
+## Cluster Isolation
+
+Namespaced resources retain the same namespace and name on each side. Cluster-scoped resources remain cluster-scoped. The built-in v2 mapper does not provide `Prefixed` or `Namespaced` isolation strategies, choose compatible provider namespaces, credentials, or dedicated clusters/workspaces instead.
+
+## Ownership and conflicts
+
+Provider instance copies carry:
+
+```yaml
+metadata:
+ labels:
+ core.kbind.io/managed: "true"
+ annotations:
+ core.kbind.io/consumer-cluster-uid: ""
+ core.kbind.io/consumer-object-uid: ""
+```
+
+The ownership check uses **both UIDs**, not just the managed label or the object's name. Instance ownership is tied to the source object, not to a Binding UID.
+
+`spec.conflictPolicy` on a binding controls a preexisting provider target:
+
+- **`Fail`** (default): leave the target untouched and report a conflict.
+- **`Adopt`**: take ownership only when both consumer ownership annotations are absent. This can overwrite fields through forced server-side apply, so use it only after reviewing the existing object.
+- Neither policy takes an object carrying another consumer/object's ownership markers. Partial markers also count as foreign ownership.
+
+A conflict produces `core.kbind.io/conflict` on the consumer instance and a Warning Event (`ForeignObjectExists` or `OwnedByAnother`). A best-effort `Synced=False` instance condition may be pruned by the API's status schema, the annotation is the durable diagnostic.
+
+Bindings count conflict annotations on a periodic reconciliation, normally every 30 seconds. `Conflicts=True` does not necessarily make `Ready` or `Synced` false, and unrelated objects can continue syncing.
+
+Deleting a conflicting consumer object does not delete the foreign provider object. Recreating an orphaned consumer instance gives it a new UID, so its retained provider copy will not automatically become owned by the new instance.
+
+## Related Secrets and ConfigMaps
+
+Add `spec.relatedResources` to either binding kind:
+
+```yaml
+relatedResources:
+ - group: ""
+ resource: secrets
+ direction: FromProvider
+ selector:
+ labelSelector:
+ matchLabels:
+ widgets.example.org/related: "true"
+ - group: ""
+ resource: configmaps
+ direction: FromConsumer
+ selector:
+ names:
+ - widget-settings
+```
+
+Use the core group (`""`), only `secrets` and `configmaps` are supported. `FromProvider` copies provider → consumer, `FromConsumer` copies consumer → provider.
+
+Selection and copying are deliberately simple:
+
+- A Binding selects within its namespace, a ClusterBinding lists across namespaces.
+- Names match exactly. If names and a label selector are both provided, **both** must match.
+- An omitted/empty selector matches everything in scope. Avoid this for Secrets.
+- Selection is independent of individual API instances. There is no JSONPath/reference following or owner-reference traversal.
+- Namespace/name are preserved. The payload fields copied are `data`, `stringData`, `binaryData`, `type`, and `immutable` when present, not arbitrary source metadata.
+- Copies carry the binding UID in `core.kbind.io/related-binding`.
+- A target with a different or absent related-binding marker is left untouched, even under `conflictPolicy: Adopt`. These collisions are silently skipped and are not included in instance conflict counts.
+
+Related-resource sync runs in binding reconciliation, normally every 30 seconds, rather than using the instance watch path. Source changes may take that long to appear.
+
+Copies belonging to the binding are deleted when they no longer match the current selector or when the binding is deleted. Source objects are not deleted. Use **one entry per resource kind and direction**: current garbage collection is keyed by binding UID and kind, not individual selector, so multiple entries for the same target kind/direction can delete each other's copies. Likewise, removing an entire entry does not run cleanup for that removed entry, clean up its copies before removing it.
+
+Related copies are not exempted by an instance's deletion policy. Namespace creation and source/target RBAC are still required. Review who can label provider Secrets before selecting them into consumers.
+
+## Heartbeat and disengagement
+
+On a successful Connection reconciliation, normally every 30 seconds, the engine creates or renews a plain `coordination.k8s.io/v1` Lease on the provider:
+
+- Namespace: the provider kubeconfig's current-context namespace, or `kbind` if unset.
+- Name: Connection name plus `-` and the first ten characters of the local cluster UID.
+- `holderIdentity`: the full consumer cluster UID.
+- `leaseDurationSeconds`: `60`.
+- Managed label and consumer-UID/Connection annotations.
+
+This heartbeat is distinct from the konnector's consumer-side leader-election Lease. Heartbeat failure is best-effort: it does not block instance sync or make the Connection unready. The core neither reaps expired Leases nor garbage-collects abandoned consumers based on them, that requires an optional [backend](../developers/backend/index.md) reaper or your own operations.
+
+When a Connection becomes not Ready, the provider cache is disengaged and its per-API instance syncers stop. When it becomes Ready again, syncers are rebuilt against the newly engaged provider. Objects and CRDs are not deleted merely because connectivity is lost. This is different from unbinding.
+
+There is no `suspend` field. Do not use credential corruption as a pause mechanism. For planned downtime, stop the konnector and understand that existing finalizers remain, for permanent removal, use the cleanup sequence below.
+
+## Unbinding and deletion
+
+### Delete one consumer instance
+
+The engine places `core.kbind.io/syncer` on the consumer instance before its first provider write. On normal deletion it:
+
+1. Reads the provider target and checks both ownership UIDs.
+2. Deletes only a copy it owns.
+3. Waits for the provider object to disappear before releasing the consumer finalizer.
+
+An unreachable provider or a provider-side finalizer can therefore hold normal instance deletion open.
+
+To keep an instance's provider copy, set the exact annotation value **before** deleting the consumer instance or unbinding:
+
+```bash
+kubectl --context=consumer -n team-a annotate widget my-widget \
+ core.kbind.io/deletion-policy=Orphan --overwrite
+```
+
+`Orphan` releases the instance finalizer without deleting the provider copy. It does not remove the provider ownership markers and does not keep a consumer instance alive when its CRD is deleted.
+
+### Delete a binding
+
+Bindings use `core.kbind.io/cleanup`. Deleting one stops it being selected for new instance sync and runs cleanup:
+
+| Deleted object | Expected cleanup when no other binding lists the API |
+| --- | --- |
+| Namespaced Binding | Delete owned provider instance copies in that namespace, release consumer instance finalizers, retain the shared CRD and consumer instances. |
+| ClusterBinding | Drain instances across the API, then delete the consumer CRD, which deletes its consumer instances. |
+| Either | Delete owned related-resource copies for the currently declared related-resource entries. |
+
+**This is destructive for a last ClusterBinding**, including a CRD installed manually with `pullPolicy: None`. The cleanup path does not limit CRD deletion to schemas originally created by that binding.
+
+Current cleanup limits:
+
+- If any other non-deleting binding anywhere lists the same API, cleanup skips that API as a whole. It does not calculate remaining namespace/Connection coverage. Overlapping bindings can therefore leave provider copies or consumer finalizers outside the remaining coverage.
+- Instance cleanup is best-effort if credentials cannot be built or provider reads fail, provider copies may remain. Actual provider delete errors can still block cleanup.
+- Unlike normal instance deletion, binding cleanup does not wait for every provider object's finalizers to finish after requesting deletion.
+- Namespaces and heartbeat Leases are not removed. An unbound schema pulled only by `pullPolicy: All` is not automatically removed on Connection deletion.
+- Editing `apis`, `connectionRef`, or related-resource entries is not a cleanup transaction for the previous spec. Remove old instances/copies deliberately before changing scope or provider.
+
+Back up resources, avoid overlapping ownership, and verify both sides after teardown.
+
+### Delete the Connection last
+
+Keep the controller running and credentials valid throughout:
+
+```bash
+kubectl --context=consumer -n team-a delete bindings.core.kbind.io widgets --timeout=120s
+kubectl --context=consumer delete connection provider --timeout=120s
+kubectl --context=consumer -n kbind delete secret provider-kubeconfig
+```
+
+Use `delete clusterbinding` instead when that is what you created.
+
+A deleting Connection waits for **all referencing bindings** to disappear. It does not delete your explicit bindings, `Ready=False` with reason `DrainingBindings` means you must finish their teardown. A Connection with `autoBind: true` explicitly deletes its generated ClusterBinding as part of deletion.
+
+After references drain, the Connection releases its own cleanup finalizer and the credential Secret's finalizer, unless another active Connection references that Secret. Do not remove the konnector or the core CRDs before this completes.
+
+Use `kubectl` for unbinding, the v2 CLI has no unbind command. Cleanup is implemented by these controllers. For stuck teardown, see [Troubleshooting](troubleshooting.md#deletion-is-stuck).
diff --git a/docs/content/usage/troubleshooting.md b/docs/content/usage/troubleshooting.md
new file mode 100644
index 000000000..f7063b904
--- /dev/null
+++ b/docs/content/usage/troubleshooting.md
@@ -0,0 +1,165 @@
+# Troubleshooting
+
+Start with the Connection, then the binding, then actual instances. A running Pod or `Synced=True` binding alone does not prove that every object was delivered.
+
+Examples below use `consumer` and `provider` context names, replace them with yours. Do not include raw kubeconfigs, tokens, Secret contents, or unredacted connection details in bug reports.
+
+## Collect the current state
+
+```bash
+kubectl --context=consumer get connections,clusterbindings
+kubectl --context=consumer get bindings.core.kbind.io -A
+kubectl --context=consumer get connection provider -o yaml
+kubectl --context=consumer -n team-a get bindings.core.kbind.io widgets -o yaml
+kubectl --context=consumer get crd widgets.example.org -o yaml
+kubectl --context=consumer -n team-a describe widget my-widget
+kubectl --context=consumer -n team-a get events --sort-by=.lastTimestamp
+kubectl --context=consumer -n kbind logs \
+ -l app.kubernetes.io/instance=konnector -c konnector --tail=200
+```
+
+For the [Quickstart](../setup/quickstart.md), substitute Connection `demo-provider`, ClusterBinding `widgets`, and namespace `default`, logs are in the host konnector terminal.
+
+Record the image tag or source commit, Kubernetes versions, schema source/policies, condition reasons/messages, and a redacted minimal reproducer.
+
+## Connection does not become Ready
+
+| Symptom | What to check |
+| --- | --- |
+| `SecretValid=False`, reason `SecretNotFound` | The referenced Secret namespace/name/key, nonempty kubeconfig data, and parse errors in the condition message. The same reason is used for malformed or unusable kubeconfig, not only missing Secrets. |
+| `Connected=False`, reason `Pending` | Provider endpoint reachability, TLS CA/server name, credential validity, and identity-read errors. |
+| `PermissionDenied=True`, reason `Forbidden` | Provider identity/discovery RBAC. Test with the same identity as the Secret, not your administrator context. |
+| `ClusterIdentityChanged` | The Secret now points to a different provider UID. Restore the original endpoint/credentials, or use a new Connection with deliberate migration. |
+| `DrainingBindings` | The Connection is deleting but explicit referencing bindings still exist. Finish their cleanup first. |
+
+The API requires an explicit namespace in `kubeconfigSecretRef`, it does not infer the konnector namespace. An omitted key defaults to `kubeconfig`.
+
+To confirm a Secret exists without exposing its data:
+
+```bash
+kubectl --context=consumer -n kbind get secret provider-kubeconfig \
+ -o custom-columns=NAME:.metadata.name,TYPE:.type
+```
+
+A workstation kubeconfig using a local exec plugin or file paths may be unusable in the stock image. A short-lived bearer token can expire, the core does not renew it.
+
+For Pod deployments, verify the provider URL is reachable from the consumer network. `https://127.0.0.1:...` refers to the consumer Pod itself, not your workstation or a kind provider node. Fix the endpoint and certificate configuration instead of disabling TLS verification.
+
+`SchemaInSync` is defined in the API but not currently emitted by the Connection reconciler. Do not wait on that condition.
+
+## An API is missing or the binding says APINotExported
+
+For `schema.source: CRD`:
+
+```bash
+kubectl --context=provider get crd widgets.example.org --show-labels
+kubectl --context=provider get crds -l core.kbind.io/exported=true
+kubectl --context=consumer get connection provider \
+ -o jsonpath='{.status.activeSchemaSource}{"\n"}{.status.exportedAPIs}{"\n"}'
+```
+
+The provider CRD must be labelled `core.kbind.io/exported: "true"`. The binding API name must be the full plural/group name, such as `widgets.example.org`, not `Widget` or `example.org/v1`.
+
+Allow about 30 seconds for discovery. If provider credentials cannot list CRDs, `Auto` currently returns that error, it does not fall back on Forbidden. Use `OpenAPI` explicitly for a CRD-less provider. If an empty label selection should export nothing, use explicit `CRD`, because Auto can fall through to OpenAPI when a successful CRD list contains no exports.
+
+For `OpenAPI`, the provider must expose usable discovery and `/openapi/v3` schemas. Kubernetes built-in groups are excluded. Missing documents or unusable schemas can cause individual resources to be skipped. The label filter does not apply to this source.
+
+Removing an export label is not a credential revocation or guaranteed cleanup operation. Revoke access using provider authorization and unbind explicitly when retiring an API.
+
+## Binding is Ready but instances do not sync
+
+Check all of the following:
+
+1. The consumer CRD exists and is Established.
+2. It has `core.kbind.io/managed: "true"` and `core.kbind.io/connection` naming the intended Connection.
+3. That Connection is Ready and has nonempty identity UIDs.
+4. A Ready binding covers the instance's namespace. A namespaced Binding cannot activate a cluster-scoped API.
+5. No overlapping ClusterBinding takes precedence over the intended Binding.
+6. Provider and consumer credentials have the required cluster-wide list/watch access.
+7. Provider create/patch and namespace-create operations are allowed.
+8. There is no foreign provider object or provider admission rejection.
+
+Read the ownership/conflict markers:
+
+```bash
+kubectl --context=consumer -n team-a get widget my-widget \
+ -o jsonpath='{.metadata.annotations.core\.kbind\.io/conflict}{"\n"}'
+kubectl --context=provider -n team-a get widget my-widget -o yaml
+```
+
+`Synced=True` reports binding setup, not a per-instance acknowledgment. In particular, with CRD source and `pullPolicy: None`, a binding can currently report success even if the externally managed consumer CRD is absent. With OpenAPI source and `None`, a preinstalled CRD also needs management markers supplied by you.
+
+Instances keep their namespace/name. A kubeconfig context's namespace only sets the heartbeat namespace, it does not re-home objects. A provider credential scoped to a different namespace is not sufficient.
+
+Provider-side per-instance failures may appear as Warning Events or logs even while binding `PermissionDenied=False`. A namespaced Binding does not make the current dynamic informers namespace-scoped.
+
+After rotating a valid kubeconfig, an existing engaged client may still use its previous credentials. Restart the konnector and recheck actual delivery. With a chart deployment, use the deployment name from:
+
+```bash
+kubectl --context=consumer -n kbind get deployment \
+ -l app.kubernetes.io/instance=konnector
+```
+
+Then run `kubectl --context=consumer -n kbind rollout restart deployment/` and wait for the rollout.
+
+## Conflicts
+
+`ForeignObjectExists` means the provider target has no consumer ownership markers. `OwnedByAnother` means its markers do not match this consumer cluster and object UID.
+
+Inspect the provider target and determine which system should own it. If intentional, `conflictPolicy: Adopt` can take a markerless target, but may overwrite its fields. It never steals another marked object's ownership. Do not remove ownership annotations simply to force adoption.
+
+The consumer conflict annotation and Events are more reliable than a custom resource's `status.conditions`, which its schema may prune. Binding conflict counts normally refresh every 30 seconds and cover instance conflicts only.
+
+## Schema or status differs from the provider
+
+CRD-source pulls retain one version and remove conversion webhooks. OpenAPI synthesis does not faithfully reproduce all validation/defaulting, referenced schemas, conversion behavior, printer columns, or subresources. Neither path installs the provider's admission webhooks or controllers.
+
+Check:
+
+- The consumer CRD storage/served version and its status subresource/schema.
+- Whether `updatePolicy: Once` intentionally pins a schema.
+- Whether `pullPolicy: None` leaves updates to your external CRD manager.
+- Whether you are using `OpenAPI + Bound`, which currently does not refresh an already-installed CRD under `Always`.
+- Provider admission errors: local schema acceptance does not prove the provider accepts a spec.
+- Whether the provider object has status at all. The demo has no real Widget controller, the quickstart patches status manually.
+
+The engine copies only bound instance `spec` up and `status` down, not arbitrary labels, annotations, or top-level payload fields. See [schema fidelity](api-concepts.md#schema-updates-and-fidelity).
+
+## Related resources do not appear
+
+Check the declared direction and source namespace. `FromProvider` means create the source on the provider, not on the consumer. The full `config/samples/widget.yaml` contains local Secret/ConfigMap manifests as well as a Widget, applying it on the consumer does not demonstrate provider-to-consumer copying.
+
+Check selectors, source/target RBAC, and target ownership. A foreign Secret/ConfigMap is silently left untouched, `Adopt` and binding instance conflict counts do not apply to these collisions. Source labels are not copied to the target.
+
+Wait for the binding's periodic reconciliation, normally 30 seconds. An omitted selector matches every object in scope. Avoid multiple selectors represented as separate entries for the same resource/direction: their garbage collection can conflict. Removing a whole related-resource entry can leave its prior copies behind.
+
+If you narrowed chart `rbac.boundResourceGroups`, add the necessary related Secret/ConfigMap permissions explicitly. The fixed credential-Secret rule alone does not allow creation/deletion of synced Secret copies.
+
+## No heartbeat Lease
+
+Look in the provider kubeconfig context namespace, or `kbind` if unset:
+
+```bash
+kubectl --context=provider get leases -A -l core.kbind.io/managed=true
+```
+
+Check Lease read/create/update and namespace permissions. Heartbeat errors do not make a healthy sync fail, the engine logs them at verbosity 2. `/readyz` is also only a process probe, not a heartbeat check.
+
+Core does not delete stale Leases or reap disconnected consumers. Those actions belong to the optional backend or your operations.
+
+## Deletion is stuck
+
+Keep the konnector running. Inspect `metadata.deletionTimestamp`, `metadata.finalizers`, Events, and controller logs on:
+
+- The consumer instance (`core.kbind.io/syncer`).
+- Its provider copy, including provider-controller finalizers.
+- The Binding/ClusterBinding (`core.kbind.io/cleanup`).
+- The Connection and referenced Secret (`core.kbind.io/cleanup`).
+
+Normal instance deletion waits for the owned provider object to be gone. Restore provider access or resolve the provider controller's cleanup failure. If retaining the provider object is intentional, the [Orphan policy](synchronization.md#delete-one-consumer-instance) releases the consumer sync finalizer once the engine can reconcile it.
+
+A Connection waits for all explicit bindings referencing it, deleting only the Connection will not delete those bindings. Delete bindings first and let them finish, then the Connection, then its Secret. Stop the controller or uninstall Helm only afterward.
+
+Binding teardown is best-effort in some provider-read/credential failure cases, so a completed delete is not proof that every remote copy disappeared. Check the provider after cleanup. Overlapping bindings and in-place scope edits can also leave old copies/finalizers behind, see [cleanup limits](synchronization.md#delete-a-binding).
+
+Do not blindly strip finalizers or delete CRDs to unblock an operation. This bypasses ownership-aware cleanup and can lose consumer data or abandon provider resources. If the controller cannot be recovered, back up the affected resources, verify ownership and remote cleanup manually, and treat any finalizer removal as an explicit disaster-recovery action.
diff --git a/docs/generators/cli-doc/main.go b/docs/generators/cli-doc/main.go
new file mode 100644
index 000000000..bb88a5963
--- /dev/null
+++ b/docs/generators/cli-doc/main.go
@@ -0,0 +1,73 @@
+/*
+Copyright 2026 The kbind Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+ "fmt"
+ "html"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ bindcmd "github.com/kbind/kbind/cli/cmd"
+)
+
+func main() {
+ root := bindcmd.New()
+ root.InitDefaultHelpFlag()
+ root.InitDefaultVersionFlag()
+ fmt.Println("# CLI Reference")
+ fmt.Println("\nGenerated from this checkout's CLI commands. Examples use the `kubectl bind` plugin. See [plugin installation](../../setup/kubectl-plugin.md).")
+ fmt.Println("\nBundle output contains provider credentials: encrypt it or use a secret manager, never commit plaintext to git. Override `--konnector-image` with an explicit v2 image, the compiled `latest` default is not a v2 guarantee.")
+ render(root)
+}
+
+func render(command *cobra.Command) {
+ fmt.Printf("\n## kubectl %s\n\n", command.CommandPath())
+ description := command.Long
+ if description == "" {
+ description = command.Short
+ }
+ fmt.Println(formatDescription(description))
+ if command.Example != "" {
+ fmt.Printf("\n```bash\n%s\n```\n", strings.TrimSpace(command.Example))
+ }
+ command.InitDefaultHelpFlag()
+ fmt.Printf("\n```text\n%s```\n", command.UsageString())
+ for _, child := range command.Commands() {
+ if !child.Hidden {
+ render(child)
+ }
+ }
+}
+
+func formatDescription(description string) string {
+ var paragraphs []string
+ for _, paragraph := range strings.Split(strings.Trim(description, "\n"), "\n\n") {
+ indent := paragraph[:len(paragraph)-len(strings.TrimLeft(paragraph, " \t"))]
+ if len(indent) >= 2 || strings.Contains(indent, "\t") {
+ lines := strings.Split(paragraph, "\n")
+ for i := range lines {
+ lines[i] = strings.TrimPrefix(lines[i], indent)
+ }
+ paragraphs = append(paragraphs, "```bash\n"+strings.Join(lines, "\n")+"\n```")
+ } else {
+ paragraphs = append(paragraphs, html.EscapeString(strings.Join(strings.Fields(paragraph), " ")))
+ }
+ }
+ return strings.Join(paragraphs, "\n\n")
+}
diff --git a/docs/generators/cli-doc/main_test.go b/docs/generators/cli-doc/main_test.go
new file mode 100644
index 000000000..9ecbf24c3
--- /dev/null
+++ b/docs/generators/cli-doc/main_test.go
@@ -0,0 +1,65 @@
+/*
+Copyright 2026 The kbind Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import "testing"
+
+func TestFormatDescription(t *testing.T) {
+ tests := []struct {
+ name string
+ description string
+ want string
+ }{
+ {
+ name: "wrapped prose",
+ description: "Connect to a provider\nand bind its APIs.",
+ want: "Connect to a provider and bind its APIs.",
+ },
+ {
+ name: "paragraph spacing",
+ description: "\nFirst paragraph.\n\nSecond\nparagraph.\n",
+ want: "First paragraph.\n\nSecond paragraph.",
+ },
+ {
+ name: "literal placeholders",
+ description: "Run kubectl bind export .",
+ want: "Run kubectl bind export <name>.",
+ },
+ {
+ name: "indented commands",
+ description: "Examples:\n\n kubectl bind catalog\n kubectl bind export widgets # keep spacing",
+ want: "Examples:\n\n```bash\nkubectl bind catalog\nkubectl bind export widgets # keep spacing\n```",
+ },
+ {
+ name: "tab indented command",
+ description: "\tkubectl bind catalog",
+ want: "```bash\nkubectl bind catalog\n```",
+ },
+ {
+ name: "empty",
+ description: "",
+ want: "",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := formatDescription(tt.description); got != tt.want {
+ t.Errorf("formatDescription() = %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/docs/generators/crd-ref/config.yaml b/docs/generators/crd-ref/config.yaml
new file mode 100644
index 000000000..c4801a197
--- /dev/null
+++ b/docs/generators/crd-ref/config.yaml
@@ -0,0 +1,7 @@
+processor:
+ ignoreTypes:
+ - "List$"
+ ignoreFields:
+ - "TypeMeta$"
+render:
+ kubernetesVersion: "1.33"
diff --git a/docs/images/logo.png b/docs/images/logo.png
new file mode 100644
index 000000000..252d3ea3f
Binary files /dev/null and b/docs/images/logo.png differ
diff --git a/docs/main.py b/docs/main.py
new file mode 100644
index 000000000..f36e64de3
--- /dev/null
+++ b/docs/main.py
@@ -0,0 +1,68 @@
+# Copyright 2024 The Kube Bind Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+
+from mkdocs_macros import fix_url
+
+
+def define_env(env):
+ """
+ This is the hook for defining variables, macros, and filters. See
+ https://mkdocs-macros-plugin.readthedocs.io/en/latest/macros/#the-define_env-function for more details.
+
+ :param env: the Jinja2 environment
+ """
+
+ @env.macro
+ def section_items(page, nav, config):
+ """
+ Returns a list of all pages that are siblings to page.
+
+ :param page: the current page. This will typically be an index.md page.
+ :param nav: the mkdocs navigation object.
+ :param config: the mkdocs config object.
+ :return: a list of all the sibling pages.
+ """
+
+ if page.parent:
+ children = page.parent.children
+ else:
+ children = nav.items
+
+ siblings = []
+ for child in children:
+ if child is page:
+ # don't include the passed in page in the list
+ continue
+ if child.is_section:
+ # don't include sections
+ continue
+ if child.file.name == 'index':
+ # don't include index pages
+ continue
+
+ # Because some pages might not have been loaded yet, we have to do so now, to get title/metadata.
+ child.read_source(config)
+
+ # Copy so we don't modify the original
+ child = copy.deepcopy(child)
+
+ # Have to fix the URL - see
+ # https://mkdocs-macros-plugin.readthedocs.io/en/latest/tips/#how-do-i-deal-with-relative-links-to-documentsimages
+ child.file.url = fix_url(child.url)
+
+ siblings.append(child)
+
+ return siblings
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
new file mode 100644
index 000000000..1247eed70
--- /dev/null
+++ b/docs/mkdocs.yml
@@ -0,0 +1,144 @@
+site_name: kbind
+site_description: "kbind v2: bind provider APIs into consumer Kubernetes clusters."
+repo_url: https://github.com/kbind-dev/kbind
+repo_name: kbind-dev/kbind
+site_url: https://docs.kbind.dev/
+
+# Site content
+docs_dir: "content"
+# Where to generate
+site_dir: "generated"
+strict: true
+
+theme:
+ name: material
+ language: en
+ # Common files such as images, stylesheets, theme overrides
+ custom_dir: "overrides"
+ features:
+ # Enable navigation section index pages, so we don't see Concepts > Concepts
+ - navigation.indexes
+ # Enable navigation tabs so we can group content by persona
+ - navigation.tabs
+ # Expand subsections by default for better visibility of content
+ - navigation.expand
+ # Show "back to top" button
+ - navigation.top
+ # Enable a copy button in code blocks
+ - content.code.copy
+ # Enable annotations on specific lines in code blocks
+ - content.code.annotate
+ # Integrate TOC into navigation sidebar
+ - toc.integrate
+ logo: logo.png
+ favicon: favicon.png
+ palette:
+ # Palette toggle for automatic mode
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+
+ # Palette toggle for light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: white
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+
+ # Palette toggle for dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ toggle:
+ icon: material/brightness-4
+ name: Switch to system preference
+
+extra:
+ version:
+ # Enable mike for multi-version selection
+ provider: mike
+
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/kbind-dev
+ - icon: fontawesome/brands/slack
+ link: https://kubernetes.slack.com/archives/C021U8WSAFK
+
+ image: high-level.png
+ title: kbind
+ description: "Bind APIs from other clusters."
+
+plugins:
+ - social:
+ cards_layout: default/variant
+ cards_layout_options:
+ logo: overrides/logo.png
+ background_color: "#1a1a1a"
+ color: "#ffffff"
+ font_family: Roboto
+ # https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin
+ # Greater control over how navigation links are shown
+ - awesome-pages
+ # Blog support
+ - blog:
+ archive: true
+ archive_name: "Recent Posts"
+ categories: false
+ archive_toc: true
+ blog_toc: true
+
+ # Docs site search
+ - search
+ # Use Jinja macros in .md files
+ - macros:
+ include_dir: "overrides"
+ module_name: "main"
+ # Configure multiple language support
+ # - i18n:
+ # docs_structure: suffix
+ # fallback_to_default: true
+ # languages:
+ # - build: true
+ # default: true
+ # locale: en
+ # name: English
+ # reconfigure_material: true
+ # reconfigure_search: true
+ # Configure multi-version plugin
+ - mike:
+ alias_type: redirect
+
+markdown_extensions:
+ # Code block highlighting
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: true
+ pygments_lang_class: true
+ # Inline code block highlighting
+ - pymdownx.inlinehilite
+ # Lets you embed content from another file
+ - pymdownx.snippets
+ # Arbitrary nesting of code/content blocks inside each other
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+
+ - admonition
+ # Enable content tabs
+ - pymdownx.tabbed:
+ alternate_style: true
+
+# Live reload if any of these change when running 'mkdocs serve'
+watch:
+ - mkdocs.yml
+ - content
+ - overrides
+
+validation:
+ links:
+ not_found: warn
+ anchors: warn
diff --git a/docs/overrides/favicon.png b/docs/overrides/favicon.png
new file mode 100644
index 000000000..d2f7a52bc
Binary files /dev/null and b/docs/overrides/favicon.png differ
diff --git a/docs/overrides/logo.png b/docs/overrides/logo.png
new file mode 100644
index 000000000..252d3ea3f
Binary files /dev/null and b/docs/overrides/logo.png differ
diff --git a/docs/overrides/logo.svg b/docs/overrides/logo.svg
new file mode 100644
index 000000000..406c259eb
--- /dev/null
+++ b/docs/overrides/logo.svg
@@ -0,0 +1,28 @@
+
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
new file mode 100644
index 000000000..85a98c41a
--- /dev/null
+++ b/docs/overrides/main.html
@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+
+{% block announce %}
+ This is the v2 development preview, not the default release.
+ Read the stable 0.x documentation.
+{% endblock %}
diff --git a/docs/overrides/partials/outdated.html b/docs/overrides/partials/outdated.html
new file mode 100644
index 000000000..f654c2fd2
--- /dev/null
+++ b/docs/overrides/partials/outdated.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% block outdated %}
+ You're not viewing the latest version.
+
+ Click here to go to latest.
+
+{% endblock %}
diff --git a/docs/overrides/partials/section-overview.html b/docs/overrides/partials/section-overview.html
new file mode 100644
index 000000000..54a0d20b2
--- /dev/null
+++ b/docs/overrides/partials/section-overview.html
@@ -0,0 +1,4 @@
+{% for item in section_items(page, navigation, config) %}
+### [{{ item.title }}]({{ item.url }})
+{{ item.meta.description or '' }}
+{% endfor %}
diff --git a/docs/overrides/stylesheets/kcp.css b/docs/overrides/stylesheets/kcp.css
new file mode 100644
index 000000000..1751630b4
--- /dev/null
+++ b/docs/overrides/stylesheets/kcp.css
@@ -0,0 +1,133 @@
+:root {
+ --md-primary-fg-color: #06c;
+}
+
+.md-sidebar--primary .md-sidebar__scrollwrap {
+ border-right: 1px solid gainsboro;
+}
+
+.md-sidebar--secondary .md-sidebar__scrollwrap {
+ border-left: 1px solid gainsboro;
+}
+
+.td-navbar {
+ padding-top: 0;
+ padding-bottom: 0;
+ min-height: 48px;
+}
+
+.td-toc {
+ padding-top: 40px;
+}
+
+.navbar-brand {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+table tr th:empty {
+ display: none;
+}
+
+/* Styles for the CRD schema reference documentation */
+/* Taken from https://github.com/giantswarm/docs/blob/main/src/static/css/crd.css */
+
+ul.crd-index {
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
+
+.crd-index .tag {
+ background-color: #ccc;
+ padding: 2px 7px;
+ font-size: 14px;
+ border-radius: 3px;
+}
+
+.crd-index .tag-provider {
+ text-transform: uppercase;
+}
+
+.crd-index .tag-provider-aws {
+ background-color: #ed9235;
+ color: #232f3b;
+}
+
+.crd-index .tag-provider-azure {
+ background-color: #1773bd;
+ color: #fff;
+}
+
+.crd-index .tag-topic {
+ background-color: #5eaebb;
+ color: #fff;
+}
+
+dl.crd-meta {
+ display: flex;
+ flex-flow: row wrap;
+ border-bottom: 1px solid #999;
+}
+
+.crd-meta dt {
+ flex-basis: 20%;
+ margin: 0;
+ padding: 4px;
+ border-top: 1px solid #999;
+ font-weight: bold;
+}
+
+.crd-meta dd {
+ flex-basis: 70%;
+ flex-grow: 1;
+ margin: 0;
+ padding: 4px;
+ border-top: 1px solid #999;
+}
+
+.crd-meta a.version {
+ margin-right: 10px;
+}
+
+.crd-schema-version {
+ border-top: 1px solid #ccc;
+}
+
+.property {
+ margin-bottom: 1em;
+}
+
+.property-header h3 {
+ font-size: 22px;
+ margin: 0 0 0 -0.1em;
+}
+
+.property-description,
+.property-meta {
+ margin: 5px 0
+}
+
+.property-type {
+ background-color: #cacaca;
+ font-size: 14px;
+ line-height: 22px;
+ padding: 3px 6px;
+ text-transform: uppercase;
+}
+
+.property-required {
+ background-color: #f5bfbf;
+ font-size: 14px;
+ line-height: 22px;
+ padding: 3px 6px;
+ text-transform: uppercase;
+}
+
+@media (min-width: 992px) {
+ .td-content>.crd-meta,
+ .td-content>.crd-description,
+ .td-content>.crd-schema-version {
+ max-width: 80%;
+ }
+
+}
diff --git a/docs/proposals/v2-extended.md b/docs/proposals/v2-extended.md
index 801555069..a83b99e0c 100644
--- a/docs/proposals/v2-extended.md
+++ b/docs/proposals/v2-extended.md
@@ -1,6 +1,6 @@
# kbind v2 Extended: Backend API, CLI, UI
-* Status: **ACCEPTED — in implementation**
+* Status: **ACCEPTED, in implementation**
* Authors: @mjudeikis
* Date: 2026-06-10 (updated 2026-07-29 for the kbind rename + root-layout restructure)
* Builds on: [v2-slim-core.md](v2-slim-core.md) (implemented on the `v2-next` branch)
@@ -16,7 +16,7 @@
The v2 core contract is up for discussion: a binding is one `kubectl apply` of a Secret +
`Connection` + bindings, consumed by the konnector, with zero kube-bind components on
-the provider. This proposal designs everything *around* that contract — the optional
+the provider. This proposal designs everything *around* that contract, the optional
service layer that answers the questions the core deliberately doesn't:
* **Who are you?** (auth: OIDC, sessions)
@@ -27,7 +27,7 @@ service layer that answers the questions the core deliberately doesn't:
* **Make it pleasant.** (CLI, UI)
The defining rule, inherited from the core: **every path through this layer terminates
-in the same one-apply bundle.** The service layer negotiates; it never syncs. If the
+in the same one-apply bundle.** The service layer negotiates, it never syncs. If the
backend is deleted the day after binding, sync is unaffected.
```
@@ -46,20 +46,20 @@ backend is deleted the day after binding, sync is unaffected.
## Goals
-* Every component independently deployable and optional; any subset works. The core
+* Every component independently deployable and optional, any subset works. The core
(GitOps-only, no extended layer at all) remains a first-class path forever.
-* The backend's terminal output is **exactly** the core's one-apply bundle — no
+* The backend's terminal output is **exactly** the core's one-apply bundle, no
intermediate request/response CRDs on the consumer, no phase-gated handshake.
* Tenancy lives here: per-consumer provider namespaces / kcp workspaces are an issuer
concern, invisible to the core (which just sees a kubeconfig whose RBAC fences it in).
-* Pluggable auth from day one — OIDC is the reference implementation, not the contract.
+* Pluggable auth from day one, OIDC is the reference implementation, not the contract.
* HA-capable by construction (the v1 in-memory-session/single-replica limitation,
roadmap #424/#488, must not survive into v2).
## Non-Goals
-* Anything that changes core sync semantics — the core contract is immutable, this layer must adapt around it.
-* Marketplace/billing/quotas (a future layer above this one; the catalog leaves room).
+* Anything that changes core sync semantics, the core contract is immutable, this layer must adapt around it.
+* Marketplace/billing/quotas (a future layer above this one, the catalog leaves room).
* Re-implementing v1's wire protocol (`BindingProvider`, `BindingResourceResponse`,
`APIServiceExportRequest` flow). v2 extended is a clean protocol.
@@ -113,7 +113,7 @@ spec:
Notes:
* The catalog is **derived-from-core-truth**: an `Export` listing an API that isn't
- actually exported (label/boundary) gets a condition; the gateway hides it. The label
+ actually exported (label/boundary) gets a condition, the gateway hides it. The label
remains the source of truth, the catalog is presentation + defaults.
* These CRDs live on the provider and are read only by the gateway/UI/CLI. The
konnector never sees them.
@@ -121,28 +121,28 @@ Notes:
### 2. Issuer (provider-side controller)
Everything v1's `backend/kubernetes` did, made a named component. The issuer is a Go
-interface (provision boundary, mint credentials, revoke); **the in-tree implementation
-is plain Kubernetes only** — the kcp issuer lives in the separate `contrib/kcp`
+interface (provision boundary, mint credentials, revoke), **the in-tree implementation
+is plain Kubernetes only**, the kcp issuer lives in the separate `contrib/kcp`
distribution, which wires its own implementation against the same interface.
-* Per consumer identity: provision the tenancy boundary — a namespace set on plain
+* Per consumer identity: provision the tenancy boundary, a namespace set on plain
Kubernetes (workspaces, in the contrib/kcp issuer).
* Mint credentials: ServiceAccount + RBAC scoped to exactly the exported APIs (+
declared related resources) within that boundary + kubeconfig. This fixes v1's
cluster-admin-ish `kube-binder` ClusterRole (roadmap #303: reduced footprint). On a
plain-Kubernetes provider "scoped to the exported APIs" is an explicit Role enumerating
- those resources; on the kcp/CRD-less flavor the tenancy boundary *is* the workspace, so
+ those resources, on the kcp/CRD-less flavor the tenancy boundary *is* the workspace, so
scoping is the workspace grant itself (everything in it is exported by construction)
- rather than a per-resource enumeration — same `Issuer` interface, two scoping mechanisms
+ rather than a per-resource enumeration, same `Issuer` interface, two scoping mechanisms
matching the core's two schema sources.
* **Credential mechanism: long-lived SA token** (v1 behavior, secret-based
ServiceAccount token). Trade-off accepted deliberately: zero rotation friction and no
- konnector-side refresh machinery, at the cost of security posture — and noting
+ konnector-side refresh machinery, at the cost of security posture, and noting
upstream Kubernetes is steering away from secret-based SA tokens, so this is
- revisitable without API change (the bundle's Secret is replaceable; a bounded-token +
+ revisitable without API change (the bundle's Secret is replaceable, a bounded-token +
reissue mode can be added later behind the same interface). Revocation = delete the
`Grant` → issuer deletes the SA/token.
-* Records issuance in **`Grant`** (`iam.kbind.io` — an issuance/identity record, not
+* Records issuance in **`Grant`** (`iam.kbind.io`, an issuance/identity record, not
catalog presentation): "identity X was issued credentials Y for export Z". The anchor
for revocation, audit, and the reaper.
@@ -150,7 +150,7 @@ distribution, which wires its own implementation against the same interface.
Stateless HTTP server (sessions externalized), serving:
-One gateway fronts exactly **one provider** — catalog aggregation across providers is a
+One gateway fronts exactly **one provider**, catalog aggregation across providers is a
future layer above this one, already possible externally because the protocol's output
is just a bundle.
@@ -159,15 +159,15 @@ is just a bundle.
| `GET /api/provider` | provider metadata + supported auth methods (successor of v1 `/api/exports`) |
| `GET /api/catalog` | `Export`s + `Collection`s visible to the caller |
| `POST /api/bind` | input: export name + consumer identity → drives issuer → returns a **one-time pickup URL** for the bundle |
-| `GET /api/bundle/` | single-use, short-TTL (5 min) bundle pickup — **returns the bundle**, then the token is dead |
-| `POST /api/apply` | optional, flag-gated: browser-apply path — gateway applies the bundle (+ konnector install) into a consumer cluster using a caller-supplied consumer kubeconfig |
+| `GET /api/bundle/` | single-use, short-TTL (5 min) bundle pickup, **returns the bundle**, then the token is dead |
+| `POST /api/apply` | optional, flag-gated: browser-apply path, gateway applies the bundle (+ konnector install) into a consumer cluster using a caller-supplied consumer kubeconfig |
| `GET /api/authorize`, `/api/callback` | auth flow (delegated to authenticator plugin) |
| `GET /api/healthz` | health |
**The bundle is the one-apply file**, delivered through a one-time pickup URL so live
credentials are fetched exactly once and never stored at rest in the gateway. Content
negotiation on pickup: `application/yaml` returns the literal multi-doc bundle (Secret +
-`Connection` + `ClusterBinding`); `application/json` wraps the same objects in a thin
+`Connection` + `ClusterBinding`), `application/json` wraps the same objects in a thin
envelope (`{ bundle: [...] }`). There is no other handshake state: no request objects to
poll, no phases to wait on. `curl` bind + pickup piped to `kubectl apply -f -` is a
complete client.
@@ -175,16 +175,16 @@ complete client.
The single-use, short-TTL property applies to the **pickup URL**, not to the credential
inside it: the bundle's Secret carries the long-lived SA token minted by the issuer
(§2), so a bundle that has been picked up once stays valid and re-appliable. That is what
-makes `-o yaml > binding.yaml` committed to git a real GitOps artifact — the pickup is
+makes `-o yaml > binding.yaml` committed to git a real GitOps artifact, the pickup is
consumed once, the token it delivered keeps working until its `Grant` is revoked.
-Re-running `bind` mints a fresh `Grant`/token and a new pickup; it does not invalidate a
+Re-running `bind` mints a fresh `Grant`/token and a new pickup, it does not invalidate a
previously committed bundle unless that `Grant` is explicitly revoked.
### 4. Auth (pluggable)
* `Authenticator` interface: `Routes()` (mounted under `/api/auth/…`) +
`Authenticate(r) (Identity, error)`. Reference implementations: OIDC (code grant +
- PKCE, as v1) and `kubernetes` (TokenReview against the provider — for in-platform
+ PKCE, as v1) and `kubernetes` (TokenReview against the provider, for in-platform
UIs that already hold a cluster identity).
* The embedded mock-OIDC server survives **only** as a dev-mode flag (`--oidc-mock`).
* OIDC is configured with kube-apiserver/kcp-style flags: `--oidc-issuer-url`,
@@ -192,11 +192,11 @@ previously committed bundle unless that `Grant` is explicitly revoked.
`--oidc-username-claim`, `--oidc-groups-claim`, `--oidc-scopes`,
`--oidc-redirect-url`.
* Sessions are **stateless encrypted cookies/tokens** (keys via
- `--cookie-signing-key`/`--cookie-encryption-key`) — no session store at all. With
- shared keys, any number of gateway replicas works out of the box; the same encrypted
+ `--cookie-signing-key`/`--cookie-encryption-key`), no session store at all. With
+ shared keys, any number of gateway replicas works out of the box, the same encrypted
blob doubles as the CLI's bearer token. (Supersedes the earlier memory/Redis
- session-store idea: the only server-side state the flow ever needed — one-time bundle
- pickup — lives on the `Grant` via annotations with optimistic concurrency, so the
+ session-store idea: the only server-side state the flow ever needed, one-time bundle
+ pickup, lives on the `Grant` via annotations with optimistic concurrency, so the
gateway keeps zero state.)
* Identity → tenancy key: `issuer + "/" + subject` hash, as v1, so the same human gets
the same boundary on re-bind.
@@ -211,13 +211,13 @@ unblocked.
* Lease expired beyond TTL → mark the issuance stale → (configurably) revoke
credentials, then delete kube-bind-created namespaces and synced objects.
* TTLs and the destructive step are opt-in and conservative by default (revoke ≠
- delete; deletion requires explicit enablement).
+ delete, deletion requires explicit enablement).
### 6. CLI (`kbind`)
-Thin client over the gateway; everything it does is reproducible by hand. Named `kbind`
+Thin client over the gateway, everything it does is reproducible by hand. Named `kbind`
(a copy/symlink as `kubectl-bind` makes it a kubectl plugin). The **krew plugin name
-stays `bind`** (kept from v1 — `kubectl krew install bind` keeps working): release
+stays `bind`** (kept from v1, `kubectl krew install bind` keeps working): release
archives ship the binary as `kubectl-bind`, and krew-release-bot renders `.krew.yaml`
against each GitHub release to PR krew-index (`.github/workflows/cli.yaml`).
@@ -232,7 +232,7 @@ kubectl bind export mangodb -o yaml > binding.yaml # GitOps mode: print, do
* `--install-konnector` (default on for interactive use) installs/upgrades the v2
konnector, as v1 did.
-* The CLI never creates bespoke objects — it applies the gateway's bundle verbatim.
+* The CLI never creates bespoke objects, it applies the gateway's bundle verbatim.
`-o yaml` output committed to git is byte-for-byte the GitOps path.
* v1 subcommands that existed to ferry the old handshake (`apiservice`, `deploy`,
per-resource polling) disappear.
@@ -248,13 +248,13 @@ Browse catalog → authenticate → bind → then either:
and the gateway's `/api/apply` applies the bundle and installs the konnector into the
consumer cluster.
-The UI is a pure gateway client; it holds no flow state the gateway doesn't have.
-Browser-apply is flag-gated on the gateway and off by default — it means consumer
+The UI is a pure gateway client, it holds no flow state the gateway doesn't have.
+Browser-apply is flag-gated on the gateway and off by default, it means consumer
credentials transit the gateway, which deployments must consciously accept.
## Packaging & repo
-Post-restructure layout (the repo root is the kbind module; `sdk/` is the standalone
+Post-restructure layout (the repo root is the kbind module, `sdk/` is the standalone
type module):
```
@@ -275,8 +275,8 @@ kbind/
```
The backend ships as **one binary with module flags** (`--enable-gateway`,
-`--enable-issuer`, `--enable-reaper`, `--enable-apply`) — operational simplicity over
-purity; the boundaries stay as Go packages so a future split costs a `main.go`, not a
+`--enable-issuer`, `--enable-reaper`, `--enable-apply`), operational simplicity over
+purity, the boundaries stay as Go packages so a future split costs a `main.go`, not a
refactor. Types the backend serves live in `sdk` (separate module) so integrations can
depend on the APIs without the server. A future kcp distribution provides its own issuer
implementation behind the same interface (it no longer lives in this repo).
@@ -287,69 +287,69 @@ implementation behind the same interface (it no longer lives in this repo).
can run side by side on one provider during transition (different endpoints, disjoint
CRD groups).
* v1 catalog objects (`APIServiceExportTemplate`/`Collection`) translate mechanically
- to `Export`/`Collection`; a converter script ships with the backend.
+ to `Export`/`Collection`, a converter script ships with the backend.
## Decided
-* **Packaging**: one `kube-bind-backend` binary; gateway/issuer/reaper/apply are module
+* **Packaging**: one `kube-bind-backend` binary, gateway/issuer/reaper/apply are module
flags, boundaries kept as Go packages.
-* **Issuance anchor**: `Grant` in `iam.kbind.io` — the typed record of
- "identity X was issued credentials Y for export Z"; anchor for revocation, audit,
+* **Issuance anchor**: `Grant` in `iam.kbind.io`, the typed record of
+ "identity X was issued credentials Y for export Z", anchor for revocation, audit,
reaper. Kept out of `catalog.kbind.io` so that group stays purely presentation+defaults.
-* **Credentials**: long-lived secret-based SA token (v1 behavior) — zero rotation
- friction accepted over security posture; revocation via `Grant` deletion; bounded
+* **Credentials**: long-lived secret-based SA token (v1 behavior), zero rotation
+ friction accepted over security posture, revocation via `Grant` deletion, bounded
tokens addable later behind the same issuer interface without API change.
* **Catalog vocabulary**: `Export` + `Collection`.
-* **Bundle delivery**: one-time pickup URL, 5-minute TTL, single use — the TTL/single-use
+* **Bundle delivery**: one-time pickup URL, 5-minute TTL, single use, the TTL/single-use
applies to the *pickup URL*, not the long-lived SA token inside, so a picked-up bundle
stays re-appliable (GitOps-safe). The bundle is never stored at rest in the gateway.
* **kcp**: stays a separate distribution (`contrib/kcp`) providing its own issuer
- implementation; the in-tree backend issuer is plain Kubernetes only.
-* **UI reach**: browser-apply path **kept** (roadmap #406) — gateway `/api/apply`
+ implementation, the in-tree backend issuer is plain Kubernetes only.
+* **UI reach**: browser-apply path **kept** (roadmap #406), gateway `/api/apply`
applies bundle + installs konnector into the consumer cluster with a caller-supplied
- kubeconfig; flag-gated, off by default, consumer credentials transiting the gateway
+ kubeconfig, flag-gated, off by default, consumer credentials transiting the gateway
is an explicitly accepted trade-off when enabled.
-* **Federation**: one gateway = one provider; cross-provider aggregation is a future
+* **Federation**: one gateway = one provider, cross-provider aggregation is a future
layer above the bundle protocol.
Added 2026-07-29 (implementation round):
* **Naming**: project is **kbind**. Groups `catalog.kbind.io` (Export, Collection) and
- `iam.kbind.io` (Grant); binaries `kbind-backend` and `kbind` (CLI, kubectl-plugin
- compatible); packages `backend/`, `cli/`, `web/` in the root module.
+ `iam.kbind.io` (Grant), binaries `kbind-backend` and `kbind` (CLI, kubectl-plugin
+ compatible), packages `backend/`, `cli/`, `web/` in the root module.
* **Sessions**: stateless encrypted cookie/bearer tokens, no session store (see §4).
HA needs only shared cookie keys across replicas.
* **One-time pickup without gateway state**: the pickup token is
- `.`; the gateway stamps `sha256(random)` + expiry as annotations
+ `.`, the gateway stamps `sha256(random)` + expiry as annotations
on the `Grant` at bind time and removes them (optimistic concurrency) on pickup. The
- bundle itself is (re)constructed on demand from the issuer's token Secret — never
+ bundle itself is (re)constructed on demand from the issuer's token Secret, never
stored at rest, single-use enforced by the API server, HA-safe.
* **Grant is spec-resolved at bind time**: the gateway copies the `Export`'s API list +
defaults into `Grant.spec`, so issuance is a stable record even if the catalog entry
changes later. The issuer controller provisions namespace/SA/RBAC from `Grant.spec`
- and reports the artifacts in `Grant.status`; deletion (revocation) unwinds via an
+ and reports the artifacts in `Grant.status`, deletion (revocation) unwinds via an
`iam.kbind.io/cleanup` finalizer.
* **Lease ↔ Grant link (per-grant)**: the issued kubeconfig pins its context namespace
to the per-consumer boundary namespace, and the konnector's heartbeat writes its
Lease into the kubeconfig's context namespace when set (falling back to the `kbind`
namespace). The Lease is named `-` and annotated
- with its Connection name; since the bundle names the Connection after the Grant, the
+ with its Connection name, since the bundle names the Connection after the Grant, the
reaper judges staleness **per grant** by that annotation. Fallback for hand-renamed
Connections is conservative: any fresh Lease in the boundary keeps its Grants alive,
only a fully silent boundary goes stale. RBAC for Leases stays scoped to the
tenant's own namespace.
-* **kubernetes authenticator**: implemented as **TokenReview** (not SAR — SAR is
- authorization; identity comes from TokenReview) against the provider, behind
- `--kubernetes-auth`. Tenancy key `kubernetes#`. No interactive routes —
+* **kubernetes authenticator**: implemented as **TokenReview** (not SAR, SAR is
+ authorization, identity comes from TokenReview) against the provider, behind
+ `--kubernetes-auth`. Tenancy key `kubernetes#`. No interactive routes,
in-platform callers just send their own bearer token.
* **Packaging (charts)**: `deploy/charts/backend` ships the service layer (module
flags as values, catalog+iam CRDs, Service, RBAC incl. `escalate`/`bind` so the
- issuer may create Roles enumerating APIs the backend itself does not hold; OIDC
+ issuer may create Roles enumerating APIs the backend itself does not hold, OIDC
client secret and cookie keys via existing Secrets).
-* **UI**: dependency-free static SPA (embedded via `go:embed` into the gateway) — no
- build toolchain in the repo; pure gateway client.
+* **UI**: dependency-free static SPA (embedded via `go:embed` into the gateway), no
+ build toolchain in the repo, pure gateway client.
## Open questions
-None — initial design questions resolved (see **Decided**). New questions raised during
+None, initial design questions resolved (see **Decided**). New questions raised during
review go here.
diff --git a/docs/proposals/v2-slim-core.md b/docs/proposals/v2-slim-core.md
index 60c88bb23..10d4da411 100644
--- a/docs/proposals/v2-slim-core.md
+++ b/docs/proposals/v2-slim-core.md
@@ -4,10 +4,10 @@
* Authors: @mjudeikis
* Date: 2026-06-10
* Supersedes parts of: [backend-only-bindings.md](backend-only-bindings.md)
-* Follow-up: [v2-extended.md](v2-extended.md) — the optional service layer (backend API,
+* Follow-up: [v2-extended.md](v2-extended.md), the optional service layer (backend API,
CLI, UI) built on this contract
-Changes to this document now require re-opening the decision log in **Decided**; the
+Changes to this document now require re-opening the decision log in **Decided**, the
remaining **Open questions** (OpenAPI fidelity spike, built-ins filter) are tracked for
implementation, not re-design.
@@ -16,11 +16,11 @@ implementation, not re-design.
kube-bind v2 splits the project into a **slim sync core** and an **optional service
layer**. The core does exactly one thing: given a Secret containing a kubeconfig, a
connection object, and a binding per API, it syncs CRDs and resource instances between a
-consumer and a provider cluster — same scope, same names, no transformation — and reports
+consumer and a provider cluster, same scope, same names, no transformation, and reports
conflicts instead of papering over them.
-Everything else — OIDC, the auth handshake, the web UI, the CLI, templates, collections,
-service-account provisioning — moves out of the core into optional, pluggable components
+Everything else, OIDC, the auth handshake, the web UI, the CLI, templates, collections,
+service-account provisioning, moves out of the core into optional, pluggable components
whose only job is to *produce the core inputs* (the kubeconfig Secret, the `Connection`,
and the `Binding`/`ClusterBinding` objects).
@@ -38,7 +38,7 @@ and the `Binding`/`ClusterBinding` objects).
│ consumed by
▼
┌────────────────────────────────────────────────────────┐
- │ core: konnector — CRD sync, spec ⇩up / status ⇩down, │
+ │ core: konnector, CRD sync, spec ⇩up / status ⇩down, │
│ related resources, conflict detection │
└────────────────────────────────────────────────────────┘
```
@@ -81,7 +81,7 @@ and the `Binding`/`ClusterBinding` objects).
a monolith, not an architecture.
4. **Sync engine inefficiencies.** Per-export dynamic controller spawning duplicates
- informers per GVR; the konnector watches *all* secrets cluster-wide; heartbeat interval
+ informers per GVR, the konnector watches *all* secrets cluster-wide, heartbeat interval
and many behaviors are hardcoded.
## Goals
@@ -89,7 +89,7 @@ and the `Binding`/`ClusterBinding` objects).
* **One apply.** A full end-to-end binding to a provider is a single
`kubectl apply -f` of one file containing all objects (Secret + `Connection` +
bindings). Nothing else, nothing more: no handshake, no waiting on phases, no ordering
- requirements — the konnector converges from whatever has been applied.
+ requirements, the konnector converges from whatever has been applied.
* Scope-preserving, name-preserving sync ("plain sync"): namespaced ↔ namespaced in the
same namespace name, cluster-scoped ↔ cluster-scoped under the same name.
* First-class conflict handling: never silently fight over or adopt objects another actor
@@ -123,20 +123,20 @@ kubectl label crd mangodbs.mangodb.io "core.kube-bind.io/exported=true"
Whatever carries the label *and* is readable by the consumer's credentials is exported.
-On **CRD-less providers** (kcp and kcp-like systems — APIs served from APIBindings /
+On **CRD-less providers** (kcp and kcp-like systems, APIs served from APIBindings /
logical clusters, no CRD objects to label), the export boundary is the **logical cluster
itself**: the kubeconfig points at a workspace that was deliberately populated with
exactly the APIs to offer, so everything discoverable there (minus built-ins) is
-exported. No label needed — curation happened when the workspace was assembled.
+exported. No label needed, curation happened when the workspace was assembled.
Either way this is the entire provider-side footprint of the core: a label (or a
-workspace boundary) plus RBAC. The credentials' RBAC *is* the authorization model; there
+workspace boundary) plus RBAC. The credentials' RBAC *is* the authorization model, there
is no separate permission/claim grant object in core.
**1. A Secret with a kubeconfig** pointing at the provider cluster, living in the
konnector's designated namespace (default `kube-bind`).
-**2. `Connection`** (cluster-scoped) — the link to one provider. Owns credentials and
+**2. `Connection`** (cluster-scoped), the link to one provider. Owns credentials and
schema delivery, and surfaces what the provider offers:
```yaml
@@ -173,9 +173,9 @@ status:
conditions: [] # SecretValid, Connected, SchemaInSync
```
-**3. `ClusterBinding`** (cluster-scoped) and **`Binding`** (namespaced) — activate
+**3. `ClusterBinding`** (cluster-scoped) and **`Binding`** (namespaced), activate
instance sync for one or more APIs, following the `ClusterRole`/`Role` convention. APIs
-are referenced by their **CRD name** on the provider (`.`) — no
+are referenced by their **CRD name** on the provider (`.`), no
group/version/resource triples to get out of sync with the schema:
```yaml
@@ -210,7 +210,7 @@ status:
boundAPIs: # per-API observed state
- name: mangodbs.mangodb.io
crdHash: sha256:… # applied schema version
- conflictCount: 0 # objects skipped due to foreign ownership;
+ conflictCount: 0 # objects skipped due to foreign ownership,
# per-object detail lives on each object's own condition
```
@@ -232,11 +232,11 @@ plain namespace RBAC, and is the v2 answer to v1's `informerScope: Namespaced`.
`Binding` listing a cluster-scoped CRD is invalid for that entry (per-API condition, no
sync). Where a `ClusterBinding` and a `Binding` cover the same API on the same
connection, the `ClusterBinding` wins and the `Binding` gets a per-API condition.
-"Bind everything" is `Connection.spec.autoBind: true` — the konnector then maintains a
+"Bind everything" is `Connection.spec.autoBind: true`, the konnector then maintains a
managed `ClusterBinding` mirroring `status.exportedAPIs`.
That is the **entire core API**: three kinds plus the Secret. No kube-bind CRDs are
-required on the provider; the konnector reads provider CRDs through the ordinary
+required on the provider, the konnector reads provider CRDs through the ordinary
apiextensions API and reads/writes instances through the dynamic client.
### The whole UX: one apply
@@ -283,7 +283,7 @@ kubectl apply -f mangodb-binding.yaml
…and the APIs are live on the consumer. Design rule this imposes: **all core objects are
order-independent and level-triggered**. A binding referencing a not-yet-existing
`Connection`, a `Connection` referencing a not-yet-existing Secret, a binding listing a
-not-yet-exported CRD — none of these are errors, all are `Pending` conditions that
+not-yet-exported CRD, none of these are errors, all are `Pending` conditions that
resolve when the missing piece arrives. No phase gating, no request/response objects, no
controller that must answer before the user may apply the next thing.
@@ -294,7 +294,7 @@ whose release requires the konnector to reach the provider through the *very* `C
being deleted. The konnector therefore drains object finalizers before a referenced
`Connection`/binding is allowed to finalize: one that still has synced objects bound to it
stays in `Terminating` with a `DrainingObjects` condition until those objects' provider
-copies are gone, then releases. "Delete the whole file" converges — it does not deadlock —
+copies are gone, then releases. "Delete the whole file" converges, it does not deadlock,
but the `Connection` is the *last* object to disappear, not the first.
### Schema sources: CRD and OpenAPI
@@ -303,30 +303,30 @@ The konnector must install a working CRD on the consumer for every bound API. Tw
to obtain it, selected by `Connection.spec.schema.source` (`Auto` probes CRD first,
falls back to OpenAPI):
-* **`CRD`** — read the CRD from the provider's apiextensions API and apply it on the
+* **`CRD`**, read the CRD from the provider's apiextensions API and apply it on the
consumer with mechanical adjustments: never copy provider-side `conversion.strategy:
- Webhook` or its caBundle (such CRDs are refused — per-API condition + skip, see the
+ Webhook` or its caBundle (such CRDs are refused, per-API condition + skip, see the
sync table), add an owner-ref to the `Connection`, and inject UX-only printer
columns/categories. To keep a multi-version CRD installable on the consumer *without* a
conversion webhook, install only the provider's **storage/served version** rather than
- every historical version. Highest fidelity; requires the provider to *have* CRDs and
+ every historical version. Highest fidelity, requires the provider to *have* CRDs and
the credentials to read them.
-* **`OpenAPI`** — for CRD-less providers (kcp and kcp-like systems, aggregated APIs):
+* **`OpenAPI`**, for CRD-less providers (kcp and kcp-like systems, aggregated APIs):
synthesize a CRD from what every Kubernetes-shaped API server already serves:
* **discovery** (`/apis//`) → plural/singular/kind/shortNames,
- categories, `namespaced`, verbs, and the served versions of the group;
+ categories, `namespaced`, verbs, and the served versions of the group,
* **`/openapi/v3/apis//`** → the structural schema per version,
- including `x-kubernetes-*` extensions and defaults;
+ including `x-kubernetes-*` extensions and defaults,
* subresource detection (status/scale) from the OpenAPI paths.
Known fidelity limits, stated honestly: CEL validation rules and other
- server-side-only constraints may not round-trip through OpenAPI — the consumer-side
+ server-side-only constraints may not round-trip through OpenAPI, the consumer-side
CRD can be *looser* than the provider's real validation, and the provider remains the
- enforcing side (a consumer object that passes locally can still be rejected upstream;
+ enforcing side (a consumer object that passes locally can still be rejected upstream,
that surfaces as a per-object sync condition, not silent loss).
-Binding by CRD name (`.`) works identically under both sources — it's
-just resource + group; with the OpenAPI source there simply is no CRD object behind the
+Binding by CRD name (`.`) works identically under both sources, it's
+just resource + group, with the OpenAPI source there simply is no CRD object behind the
name on the provider. `status.exportedAPIs` is computed per source: labeled CRDs for
`CRD`, discovery (minus built-in groups) for `OpenAPI`.
@@ -335,16 +335,16 @@ name on the provider. `status.exportedAPIs` is computed per source: labeled CRDs
| Aspect | v2 behavior |
|---|---|
| Identity | `ns/name` on consumer == `ns/name` on provider. Cluster-scoped stays cluster-scoped. No prefixes, no remapping, no `APIServiceNamespace`. |
-| Selection | `ClusterBinding` syncs all instances of its listed APIs cluster-wide; namespaced `Binding` syncs only instances in its own namespace. `Connection.spec.autoBind` binds everything exported. |
+| Selection | `ClusterBinding` syncs all instances of its listed APIs cluster-wide, namespaced `Binding` syncs only instances in its own namespace. `Connection.spec.autoBind` binds everything exported. |
| Spec | consumer → provider, server-side apply with a dedicated field manager. |
| Status | provider → consumer, status subresource update. |
-| Namespaces | If the consumer namespace doesn't exist on the provider, the konnector creates it (annotated as kube-bind-created; only kube-bind-created namespaces are cleaned up on unbind). |
-| Deletion | Finalizer on consumer object (as today); consumer deletion propagates to provider, finalizer released only when the provider copy is fully gone (a provider-side finalizer holds the consumer object in `Terminating` until it clears). `kube-bind.io/deletion-policy: Orphan` on a consumer object releases the finalizer without deleting the provider copy. Provider-side deletion of a synced object is treated as drift and re-created **unless** the provider copy has a non-zero deletionTimestamp — then the konnector waits for it to finalize rather than racing a re-create. Consumer is source of truth for spec. |
-| Schema | Per `Connection.spec.schema`: konnector obtains schemas via `source: CRD` (read apiextensions) or `OpenAPI` (synthesize from discovery + `/openapi/v3` — CRD-less providers like kcp), pulls per `pullPolicy: All` or `Bound`, applies on the consumer (owner-ref to the `Connection`). `updatePolicy: Always` keeps following provider schema changes; `Once` pins. CRDs with `strategy: Webhook` are refused (per-API condition + skip). |
-| Discovery | `Connection.status.exportedAPIs`: labeled CRDs (`source: CRD`) or discovery minus built-ins (`source: OpenAPI`, where the logical-cluster boundary is the export boundary) — core-level discovery with zero provider CRDs. |
-| Related resources | Selected Secrets/ConfigMaps sync in the declared direction, same identity rules, scoped like their binding. They are owned by the **binding** (not by individual instances): an object is synced while it matches the selector and is garbage-collected when it stops matching or the binding is removed. The same ownership markers and `conflictPolicy` apply — a related object already owned by another binding/consumer is a conflict, never silently overwritten. |
-| RBAC | The supplied credentials' RBAC *is* the authorization model; partial RBAC is an expected steady state, not a failure. Each forbidden operation surfaces as a typed `PermissionDenied` condition naming the verb+resource refused (per-API on the binding, per-object on the instance); the konnector keeps syncing everything it *is* allowed to and never fails a whole binding because one resource or namespace is out of reach. |
-| Informers | One shared dynamic informer per GVR per connection. Cluster-wide informers for `ClusterBinding`; namespace-scoped informers where only namespaced `Binding`s exist. |
+| Namespaces | If the consumer namespace doesn't exist on the provider, the konnector creates it (annotated as kube-bind-created, only kube-bind-created namespaces are cleaned up on unbind). |
+| Deletion | Finalizer on consumer object (as today), consumer deletion propagates to provider, finalizer released only when the provider copy is fully gone (a provider-side finalizer holds the consumer object in `Terminating` until it clears). `kube-bind.io/deletion-policy: Orphan` on a consumer object releases the finalizer without deleting the provider copy. Provider-side deletion of a synced object is treated as drift and re-created **unless** the provider copy has a non-zero deletionTimestamp, then the konnector waits for it to finalize rather than racing a re-create. Consumer is source of truth for spec. |
+| Schema | Per `Connection.spec.schema`: konnector obtains schemas via `source: CRD` (read apiextensions) or `OpenAPI` (synthesize from discovery + `/openapi/v3`, CRD-less providers like kcp), pulls per `pullPolicy: All` or `Bound`, applies on the consumer (owner-ref to the `Connection`). `updatePolicy: Always` keeps following provider schema changes, `Once` pins. CRDs with `strategy: Webhook` are refused (per-API condition + skip). |
+| Discovery | `Connection.status.exportedAPIs`: labeled CRDs (`source: CRD`) or discovery minus built-ins (`source: OpenAPI`, where the logical-cluster boundary is the export boundary), core-level discovery with zero provider CRDs. |
+| Related resources | Selected Secrets/ConfigMaps sync in the declared direction, same identity rules, scoped like their binding. They are owned by the **binding** (not by individual instances): an object is synced while it matches the selector and is garbage-collected when it stops matching or the binding is removed. The same ownership markers and `conflictPolicy` apply, a related object already owned by another binding/consumer is a conflict, never silently overwritten. |
+| RBAC | The supplied credentials' RBAC *is* the authorization model, partial RBAC is an expected steady state, not a failure. Each forbidden operation surfaces as a typed `PermissionDenied` condition naming the verb+resource refused (per-API on the binding, per-object on the instance), the konnector keeps syncing everything it *is* allowed to and never fails a whole binding because one resource or namespace is out of reach. |
+| Informers | One shared dynamic informer per GVR per connection. Cluster-wide informers for `ClusterBinding`, namespace-scoped informers where only namespaced `Binding`s exist. |
### Conflict handling (the new hard part)
@@ -352,38 +352,38 @@ Identity mapping means two writers can legitimately collide. Core rules:
1. Every object the konnector writes carries ownership markers: the **binding UID** plus
the **source cluster UID**. The source cluster UID is the identity the `Connection`
- pins in its status the first time it resolves its credentials — *not* something read
+ pins in its status the first time it resolves its credentials, *not* something read
from a fixed object like the `kube-system` namespace, which does not exist on
CRD-less/logical-cluster providers (kcp). This keeps the marker well-defined under both
schema sources. (Same idea as today's `kube-bind.io/consumer-uid` / `provider-uid`
annotations, kept.)
2. Before first write, the konnector classifies the existing target object three ways:
* **No markers** (a foreign, un-owned object): `conflictPolicy: Fail` (default) does
- not touch it; `conflictPolicy: Adopt` stamps markers and SSA force-applies.
- * **Our markers, our current binding + object UID**: ours — normal SSA update.
+ not touch it, `conflictPolicy: Adopt` stamps markers and SSA force-applies.
+ * **Our markers, our current binding + object UID**: ours, normal SSA update.
* **Our cluster's markers but a stale/foreign binding-or-object UID**, *or* **another
cluster's markers**: always a conflict, regardless of `conflictPolicy`. `Adopt`
- never steals an object another binding/consumer owns; first-writer-wins,
+ never steals an object another binding/consumer owns, first-writer-wins,
deterministically.
3. Conflicts are surfaced by **two authoritative signals**: a Kubernetes **Event** on the
consumer object, and a **count + `Conflicts` condition on the binding**
- (`boundAPIs[].conflictCount`) — never an unbounded list of object names. Both carry the
- reason that tells an operator how to remediate: `ForeignObjectExists` (no markers;
+ (`boundAPIs[].conflictCount`), never an unbounded list of object names. Both carry the
+ reason that tells an operator how to remediate: `ForeignObjectExists` (no markers,
rename or switch to `Adopt`) vs. `OwnedByAnother` (claimed by a different
- binding/consumer; pick another name). The konnector *also* writes that reason as a
+ binding/consumer, pick another name). The konnector *also* writes that reason as a
condition on the conflicting object itself, but only **best-effort**: a structural CRD
whose `status` schema has no `conditions` field will have it pruned by the API server
(confirmed in the POC against a status-only CRD). So the Event and the binding count are
- the contract; the per-object condition is a convenience present only when the synced
+ the contract, the per-object condition is a convenience present only when the synced
type defines `status.conditions`. The konnector keeps syncing everything else.
4. Field-level conflicts within an owned object are resolved by SSA with
`force=true` for our field manager on our sync direction only (spec fields upstream,
- status fields downstream) — same as today, but now stated as the contract.
+ status fields downstream), same as today, but now stated as the contract.
### Topology: the konnector is the only running component
-Consumer-side pull, as today — and **the core has no backend**. The konnector is the
-single process in the entire core path; everything v1's backend did for sync either
+Consumer-side pull, as today, and **the core has no backend**. The konnector is the
+single process in the entire core path, everything v1's backend did for sync either
moved into the konnector or out of core:
| v1 backend job | v2 home |
@@ -392,12 +392,12 @@ moved into the konnector or out of core:
| materialize exports/schemas (`BoundSchema`) | konnector reads provider CRDs via apiextensions API directly |
| create provider namespaces (`APIServiceNamespace` controller) | konnector creates them (iff RBAC allows) |
| track consumer liveness (`ClusterBinding` heartbeat) | konnector maintains a `Lease` on the provider |
-| mint kubeconfigs / service accounts, rotate credentials | **out of core** — service layer or manual |
-| GC artifacts of dead/vanished consumers | **out of core** — service layer can reap based on expired Leases |
+| mint kubeconfigs / service accounts, rotate credentials | **out of core**, service layer or manual |
+| GC artifacts of dead/vanished consumers | **out of core**, service layer can reap based on expired Leases |
* The konnector runs in the consumer cluster (or out-of-cluster with a consumer
- kubeconfig — it must not assume in-cluster). One konnector may serve many consumer
- logical clusters (kcp workspaces, a cluster fleet) — see
+ kubeconfig, it must not assume in-cluster). One konnector may serve many consumer
+ logical clusters (kcp workspaces, a cluster fleet), see
[Engine](#engine-built-on-multicluster-runtime).
* It watches `Connection`, `ClusterBinding`, and `Binding` objects, and only the Secrets
that `Connection`s reference (fixes the v1 watch-all-secrets issue). A `Connection`'s
@@ -405,14 +405,14 @@ moved into the konnector or out of core:
`kube-bind`): `Connection` is cluster-scoped, so letting it name a Secret in any
namespace would let anyone who can create a `Connection` read any Secret the konnector's
ServiceAccount can. Cross-namespace refs are rejected with `SecretValid=False`.
-* One `Connection` = one provider client/informer context; all bindings referencing it
+* One `Connection` = one provider client/informer context, all bindings referencing it
share that context.
* The provider cluster needs: reachable API server + RBAC for the supplied credentials.
No backend, no controllers, no CRDs.
The honest cost of zero provider-side runtime: credential issuance/rotation and cleanup
after consumers that disappear forever are nobody's job in core. The Lease per
-`Connection` is the hook — an optional provider-side reaper (service layer) can GC
+`Connection` is the hook, an optional provider-side reaper (service layer) can GC
kube-bind-created namespaces and synced objects whose Lease has expired.
Heartbeat keeps the zero-CRD property: the konnector maintains a plain
@@ -424,7 +424,7 @@ component in the service layer.
The v2 konnector is built on
[multicluster-runtime](https://github.com/kubernetes-sigs/multicluster-runtime)
-(roadmap #299), not hand-rolled informer plumbing — and structured as a **bridge between
+(roadmap #299), not hand-rolled informer plumbing, and structured as a **bridge between
two cluster sets**, each fronted by an mcr provider:
* **Provider side**: a custom mcr provider discovers "clusters" from `Connection`
@@ -432,25 +432,25 @@ two cluster sets**, each fronted by an mcr provider:
resolves, disengaged on deletion).
* **Consumer side**: also behind an mcr provider abstraction. The default is the trivial
single-cluster provider (today's shape: one konnector in one consumer cluster). But
- nothing in the engine assumes one consumer cluster — swapping the consumer-side
+ nothing in the engine assumes one consumer cluster, swapping the consumer-side
provider scales the same binary out:
- * **kcp provider** → one konnector per kcp instance, serving *every* workspace; each
+ * **kcp provider** → one konnector per kcp instance, serving *every* workspace, each
workspace carries its own `Connection`/`Binding` objects and is its own sync domain.
* **fleet provider** (kubeconfig/cluster-inventory) → one konnector serving many
physical consumer clusters, connecting a *set* of consumer clusters to a *set* of
providers.
* The sync domain is always the pair *(consumer logical cluster, `Connection` within
- it)* — the core API is unchanged regardless of how many consumer clusters one
+ it)*, the core API is unchanged regardless of how many consumer clusters one
konnector serves. Multi-consumer is purely an engine/deployment dimension, never an
API dimension.
-* Sync controllers are written once as multi-cluster-aware reconcilers; per-connection
+* Sync controllers are written once as multi-cluster-aware reconcilers, per-connection
client/cache lifecycle, informer dedup per GVR, and teardown come from the framework
instead of v1's contextstore + dynamic controller spawning.
* The backend already uses multicluster-runtime (`mcmanager` + the kcp apiexport
- provider) — v2 aligns consumer and provider sides on one runtime model, and the kcp
+ provider), v2 aligns consumer and provider sides on one runtime model, and the kcp
flavor falls out of swapping the mcr provider rather than special-casing the engine.
-Alpha ships the single-cluster consumer provider only; the kcp and fleet consumer
+Alpha ships the single-cluster consumer provider only, the kcp and fleet consumer
providers are the explicit scale-out path, unlocked by the architecture rather than
designed later.
@@ -466,7 +466,7 @@ forking the engine:
type Mapper interface {
// ToProvider maps a consumer object key to its provider key.
ToProvider(gvr schema.GroupVersionResource, key ObjectKey) (ObjectKey, error)
- // ToConsumer is the inverse; must round-trip.
+ // ToConsumer is the inverse, must round-trip.
ToConsumer(gvr schema.GroupVersionResource, key ObjectKey) (ObjectKey, error)
}
```
@@ -476,10 +476,10 @@ Notes:
* This is a *compile-time* extension (custom konnector build), not a CRD-configurable
one. Keeping it out of the API keeps the API honest: the core CRD never promises
renaming.
-* The v1 `Prefixed` behavior is implementable as a Mapper; `Namespaced` (scope
- conversion) is intentionally **not** implementable — the interface maps keys, it cannot
+* The v1 `Prefixed` behavior is implementable as a Mapper, `Namespaced` (scope
+ conversion) is intentionally **not** implementable, the interface maps keys, it cannot
change scope. That's the hard line v2 draws.
-* Possible second interface later: `Transformer` (mutate object payload before write —
+* Possible second interface later: `Transformer` (mutate object payload before write,
label injection, field stripping). Deliberately deferred.
### Extension point 2: the handshake
@@ -491,12 +491,12 @@ becomes "anything that can create a Secret, a `Connection`, and bindings":
|---|---|
| backend HTTP API, OIDC, sessions, SPA/UI | optional `kube-bind-backend` component (own module/repo dir, own release cycle). Its output is exactly the core objects. |
| `kubectl bind` CLI + browser dance | optional `cli/` plugin, talks to the backend, ends by applying the core objects + (optionally) installing the konnector. |
-| `APIServiceExportTemplate`, `Collection` | backend-layer CRDs (rich catalog: descriptions, grouping, permission review). Raw discovery is core (`Connection.status.exportedAPIs`); curation is service layer. |
+| `APIServiceExportTemplate`, `Collection` | backend-layer CRDs (rich catalog: descriptions, grouping, permission review). Raw discovery is core (`Connection.status.exportedAPIs`), curation is service layer. |
| `APIServiceExportRequest`, `BindableResourcesRequest`, `BindingResourceResponse` | backend-layer wire/handshake types. |
| `APIServiceBindingBundle` ("bind everything") | absorbed into core as `Connection.spec.autoBind: true` (managed `ClusterBinding` mirroring `exportedAPIs`). |
| service-account + kubeconfig minting ([backend/kubernetes/resources/](../../backend/kubernetes/resources/)) | backend layer (it's credential issuance, i.e. auth). |
-| kcp integration ([contrib/kcp/](../../contrib/kcp/)) | a backend *flavor*: kcp workspaces give per-consumer isolation for free, which is exactly what identity-mapping core wants. The core already speaks to kcp-like providers natively via `schema.source: OpenAPI` (no CRDs needed); the backend flavor only adds workspace provisioning/handshake. Likely the best-fit v2 deployment. |
-| heartbeat / `ClusterBinding` | core keeps a plain `Lease` on the provider; anything richer is an optional provider-side visibility component. |
+| kcp integration ([contrib/kcp/](../../contrib/kcp/)) | a backend *flavor*: kcp workspaces give per-consumer isolation for free, which is exactly what identity-mapping core wants. The core already speaks to kcp-like providers natively via `schema.source: OpenAPI` (no CRDs needed), the backend flavor only adds workspace provisioning/handshake. Likely the best-fit v2 deployment. |
+| heartbeat / `ClusterBinding` | core keeps a plain `Lease` on the provider, anything richer is an optional provider-side visibility component. |
GitOps is the degenerate case: a human or pipeline commits the one-apply file (Secret +
`Connection` + bindings), no optional layer at all.
@@ -506,17 +506,17 @@ GitOps is the degenerate case: a human or pipeline commits the one-apply file (S
* `Isolation` / `ClusterScopedIsolation` / `informerScope` fields and the
isolation strategy implementations (`prefixed.go`, `namespaced.go`, `none.go`).
* `APIServiceNamespace` and the namespace-lifecycle controller.
-* `BoundSchema` (core reads provider CRDs directly; the provider-side copy-of-a-CRD
+* `BoundSchema` (core reads provider CRDs directly, the provider-side copy-of-a-CRD
object disappears).
* `ClusterBinding` from core (possibly resurrected in the backend layer).
* Embedded OIDC, sessions, cookies, SPA from anything called "core".
-* The hardcoded claimable-APIs list (`claimable_apis.go`) — replaced by
+* The hardcoded claimable-APIs list (`claimable_apis.go`), replaced by
`relatedResources` limited to `secrets`/`configmaps`, label + named selectors only.
### Repo & module layout (new group, same repo, `v2/` prefix)
-One rule: **the path tells you the version**. Everything v2 lives under `v2/`; every
-path outside it is v1 by definition — frozen on main, maintained on a release branch,
+One rule: **the path tells you the version**. Everything v2 lives under `v2/`, every
+path outside it is v1 by definition, frozen on main, maintained on a release branch,
deleted from main at v2 GA.
```
@@ -531,8 +531,8 @@ kube-bind/
│ ├── backend/ # future optional layer (follow-up proposal)
│ └── cli/ # future optional layer (follow-up proposal)
│
-├── sdk/apis/kubebind/v1alpha2/ # v1 — frozen; types kept on main for consumers
-├── pkg/ cmd/ backend/ cli/ web/ # v1 — frozen on main, fixes on release-1.x,
+├── sdk/apis/kubebind/v1alpha2/ # v1, frozen, types kept on main for consumers
+├── pkg/ cmd/ backend/ cli/ web/ # v1, frozen on main, fixes on release-1.x,
└── contrib/kcp/ # deleted from main at v2 GA
```
@@ -540,18 +540,18 @@ Rules:
* **No imports across the boundary, in either direction**: nothing under `v2/` imports
v1 packages, nothing outside `v2/` imports v2 packages. Enforced in CI. Shared code is
- copied, not linked — the freedom to diverge is the point of the split.
+ copied, not linked, the freedom to diverge is the point of the split.
* `v2/sdk` is a type-only module so consumers (backends, integrations) can depend on the
- API without pulling the engine; `v2/konnector` depends on `v2/sdk`, never vice versa.
+ API without pulling the engine, `v2/konnector` depends on `v2/sdk`, never vice versa.
* Within v2, optional layers (`v2/backend`, `v2/cli`) depend on `v2/sdk` (and at most
the konnector's library surface), never the other way.
* The v2 konnector binary serves **only** `core.kube-bind.io`. v1alpha2 konnector is
- maintained on the release branch until deprecation; no dual-stack binary.
+ maintained on the release branch until deprecation, no dual-stack binary.
* Engine implementation: built on multicluster-runtime (see
- [Engine](#engine-built-on-multicluster-runtime)); one shared informer set per provider
+ [Engine](#engine-built-on-multicluster-runtime)), one shared informer set per provider
connection, no hand-rolled informer plumbing.
* Images follow the same rule: `ghcr.io/kube-bind/konnector:v2.*` is built from
- `v2/konnector`; `v1.*` / `v0.*` tags only ever come from the release branch.
+ `v2/konnector`, `v1.*` / `v0.*` tags only ever come from the release branch.
## Migration
@@ -561,77 +561,77 @@ Rules:
that, for bindings using `None` isolation + identity-compatible layouts, generates the
equivalent v2 objects. Anything using `Prefixed`/`Namespaced` isolation cannot migrate
to core semantics and stays on v1 / moves to a per-consumer provider deployment first.
-* v1alpha2 enters maintenance on the day v2 core reaches alpha; removal horizon TBD.
+* v1alpha2 enters maintenance on the day v2 core reaches alpha, removal horizon TBD.
## Decided
* **One apply**: a complete e2e binding is a single `kubectl apply -f` of one file
- (Secret + `Connection` + bindings); all core objects are order-independent and
- level-triggered — missing references are `Pending` conditions, never errors.
+ (Secret + `Connection` + bindings), all core objects are order-independent and
+ level-triggered, missing references are `Pending` conditions, never errors.
* **Naming**: group `core.kube-bind.io`, kinds `Connection`, `ClusterBinding`, `Binding`.
The agent stays **konnector**.
* **Binding shape & scope**: a binding lists one or more CRD names (`spec.apis`) plus
- its related resources; cluster-wide vs per-namespace is expressed by kind
+ its related resources, cluster-wide vs per-namespace is expressed by kind
(`ClusterBinding`/`Binding`), not by fields.
-* **Bind everything**: `Connection.spec.autoBind: true` — konnector maintains a managed
+* **Bind everything**: `Connection.spec.autoBind: true`, konnector maintains a managed
`ClusterBinding` mirroring `status.exportedAPIs`. Replaces `APIServiceBindingBundle`.
* **Discovery**: provider opt-in via `core.kube-bind.io/exported=true` CRD label on
- plain Kubernetes; on CRD-less providers (kcp, kcp-like) the logical-cluster boundary
+ plain Kubernetes, on CRD-less providers (kcp, kcp-like) the logical-cluster boundary
is the export boundary (discovery minus built-ins). Exported APIs surfaced on
`Connection.status.exportedAPIs`. No catalog CRDs in core.
* **Schema delivery**: owned by `Connection` (`source`, `pullPolicy`, `updatePolicy`).
`source: Auto` (default) reads provider CRDs when present, else synthesizes CRDs from
- discovery + `/openapi/v3` — so kcp-like, CRD-less providers work in core.
- `pullPolicy` defaults to `Bound` — CRDs are installed only when a binding references
- them; `All` is the eager opt-in.
+ discovery + `/openapi/v3`, so kcp-like, CRD-less providers work in core.
+ `pullPolicy` defaults to `Bound`, CRDs are installed only when a binding references
+ them, `All` is the eager opt-in.
* **Conversion webhooks**: CRDs with `strategy: Webhook` are refused in core alpha
- (per-API condition + skip); revisit on demand.
-* **Sync direction**: fixed — spec consumer→provider, status provider→consumer. A
+ (per-API condition + skip), revisit on demand.
+* **Sync direction**: fixed, spec consumer→provider, status provider→consumer. A
`syncMode` field name is reserved per-API but not implemented in alpha.
-* **Related resources**: `secrets` + `configmaps` only; `labelSelector` + named
+* **Related resources**: `secrets` + `configmaps` only, `labelSelector` + named
selectors only. JSONPath reference-following selectors do not enter core.
* **Conflicts**: `conflictPolicy: Fail | Adopt` with three-way classification (no markers
/ ours / foreign-or-stale). `Adopt` only takes *un-owned* objects and never steals one
- carrying another binding's/consumer's markers — cross-binding collisions are always
+ carrying another binding's/consumer's markers, cross-binding collisions are always
conflicts, first-writer-wins. Conflicts are surfaced authoritatively by a Kubernetes
**Event** on the consumer object plus a **`Conflicts` condition + `conflictCount`** on
- the binding (reasons `ForeignObjectExists` vs `OwnedByAnother`); a per-object condition
+ the binding (reasons `ForeignObjectExists` vs `OwnedByAnother`), a per-object condition
is written best-effort only (the API server prunes it on CRDs whose `status` schema has
- no `conditions` field — confirmed in the POC). The marker's source cluster UID is the
+ no `conditions` field, confirmed in the POC). The marker's source cluster UID is the
`Connection`-pinned identity, so conflicts stay well-defined on CRD-less providers too.
* **Cluster identity**: each `Connection` pins the resolved provider (and local) cluster
- UID in its status on first connect and is immutable thereafter; a Secret later pointing
+ UID in its status on first connect and is immutable thereafter, a Secret later pointing
at a *different* cluster is rejected (`Connected=False`) rather than silently re-homing
synced objects.
* **Deletion**: consumer deletion propagates to the provider and the binding/`Connection`
- drains object finalizers before finalizing (`DrainingObjects`); `deletion-policy:
+ drains object finalizers before finalizing (`DrainingObjects`), `deletion-policy:
Orphan` opts an object out of provider-side deletion.
* **Provider namespaces**: konnector creates missing namespaces iff RBAC allows
- (annotated kube-bind-created, cleaned up on unbind); otherwise condition + wait.
+ (annotated kube-bind-created, cleaned up on unbind), otherwise condition + wait.
* **Heartbeat**: a plain `coordination.k8s.io/Lease` per Connection, maintained by the
konnector in a designated provider namespace. Still zero kube-bind CRDs on the
provider.
-* **Mapper**: identity only in-tree; compile-time interface for out-of-tree key mapping;
+* **Mapper**: identity only in-tree, compile-time interface for out-of-tree key mapping,
scope conversion impossible by construction.
-* **Topology**: consumer-side pull; the konnector is the **only running component** in
- the core — no backend required. Provider needs zero kube-bind CRDs, controllers, or
+* **Topology**: consumer-side pull, the konnector is the **only running component** in
+ the core, no backend required. Provider needs zero kube-bind CRDs, controllers, or
processes. Provider-side credential lifecycle and dead-consumer GC are service-layer
jobs (keyed off the Lease).
-* **Engine**: built on multicluster-runtime as a bridge between two cluster sets — a
+* **Engine**: built on multicluster-runtime as a bridge between two cluster sets, a
custom mcr provider turns each `Connection` into a logical provider cluster, and the
- consumer side sits behind an mcr provider too (single-cluster in alpha; kcp provider =
- one konnector per kcp instance serving all workspaces; fleet provider = sets of
+ consumer side sits behind an mcr provider too (single-cluster in alpha, kcp provider =
+ one konnector per kcp instance serving all workspaces, fleet provider = sets of
consumer clusters to sets of providers). The sync domain is always *(consumer logical
- cluster, Connection)*; multi-consumer is an engine/deployment dimension, never an API
+ cluster, Connection)*, multi-consumer is an engine/deployment dimension, never an API
dimension. Sync controllers are multi-cluster-aware reconcilers, no hand-rolled
informer/contextstore machinery.
-* **Repo**: new API group, same repo, `v2/` prefix directory — the path tells you the
- version. `v2/sdk` (types) + `v2/konnector` (engine) as separate Go modules; no imports
- across the v1/v2 boundary in either direction (CI-enforced); v1 paths frozen on main
+* **Repo**: new API group, same repo, `v2/` prefix directory, the path tells you the
+ version. `v2/sdk` (types) + `v2/konnector` (engine) as separate Go modules, no imports
+ across the v1/v2 boundary in either direction (CI-enforced), v1 paths frozen on main
and deleted at v2 GA.
-* **Dual-stack**: none. v2 konnector serves only `core.kube-bind.io`; the v1alpha2
+* **Dual-stack**: none. v2 konnector serves only `core.kube-bind.io`, the v1alpha2
konnector is maintained on a release branch until deprecation.
-* **Backend/UI/CLI redesign**: separate follow-up proposal; this doc only fixes the
+* **Backend/UI/CLI redesign**: separate follow-up proposal, this doc only fixes the
contract boundary.
## Open questions
@@ -639,11 +639,11 @@ Rules:
1. **OpenAPI synthesis fidelity.** How lossy is discovery + `/openapi/v3` → CRD in
practice (CEL rules, defaulting edge cases, multi-version with conversion)? Needs a
spike against kcp and a kcp-like system (e.g. kplane) before the `Auto` default is
- locked. Mitigation already in the contract: the provider is the enforcing side;
+ locked. Mitigation already in the contract: the provider is the enforcing side,
upstream rejections surface as per-object sync conditions.
Do we need different way to deliver schema as we do now in v1?
2. **Built-ins filter for `source: OpenAPI`.** Exact exclusion list/heuristic for "minus
- built-ins" in `exportedAPIs` (core groups, `*.k8s.io`, kcp's own groups?) — and
+ built-ins" in `exportedAPIs` (core groups, `*.k8s.io`, kcp's own groups?), and
whether it should be overridable on the `Connection`.
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 000000000..d1ad46659
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,7 @@
+mike==2.1.3
+mkdocs==1.6.1
+mkdocs-material[imaging]==9.7.1
+mkdocs-awesome-pages-plugin==2.9.2
+mkdocs-macros-plugin==1.0.5
+pymdown-extensions==10.21.3
+click==8.1.8
diff --git a/docs/scripts/deploy-docs.sh b/docs/scripts/deploy-docs.sh
new file mode 100644
index 000000000..2633e1eeb
--- /dev/null
+++ b/docs/scripts/deploy-docs.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
+cd "$REPO_ROOT/docs"
+
+if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then
+ echo "Pull requests must build documentation without deploying." >&2
+ exit 1
+fi
+
+remote=${REMOTE:-origin}
+branch=${BRANCH:-gh-pages}
+options=(--remote "$remote" --branch "$branch")
+
+if [[ "${PUSH:-0}" == "1" ]]; then
+ options+=(--push)
+fi
+
+git fetch "$remote" "$branch"
+message=${DOCS_COMMIT_MESSAGE:-Publish v2 documentation preview}
+mike deploy "${options[@]}" --message "$message" --title "v2 (preview)" v2