An application team runs on a NATS cluster someone else deployed, a Helm release in messaging, and wants the streams its services created at runtime declared as resources without recreating them.
The connection #
Same shape as a managed NATS cluster’s. The credentials come from whoever runs that NATS cluster’s auth plane.
01-natsconnection.yaml
apiVersion: nats.mikluko.io/v1beta1
kind: NatsConnection
metadata:
name: shared-payments
namespace: payments
spec:
servers: ["tls://nats.messaging.svc:4222"]
tls:
ca:
# `key` defaults to ca.crt, the key cert-manager writes.
secretKeyRef:
name: messaging-nats-ca
# Minted by whoever runs that NATS cluster's auth plane and delivered here
# by them. The account is whichever one these credentials sign into.
credentials:
secretKeyRef:
name: payments-secrets
key: nats.creds # defaults to user.creds, the key NatsUser writes
kubectl apply -f https://nats-operator.io/docs/stories/03-unmanaged/01-natsconnection.yamlAdopting streams #
Adopt takes over a stream that must already exist and writes the server’s config into the spec. AdoptOrCreate takes it over or creates it, applying the spec as written. The default Never refuses to touch a stream the controller does not own.
01-natsstreams.yaml
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsStream
metadata:
name: payments
namespace: payments
spec:
connectionRef:
name: shared-payments
# The stream must already exist; if it does not, the resource waits
# (Adopted=False, reason NotFound) and never creates it. On adoption the
# controller writes the server's config into this spec, so the spec needs
# only what identifies the stream. See 01-live-natsstream-payments.yaml.
adoptionPolicy: Adopt
name: PAYMENTS
---
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsStream
metadata:
name: refunds
namespace: payments
spec:
connectionRef:
name: shared-payments
# Adopt if it exists, create it if not. The spec as written is applied to the
# server either way; the controller then writes the fields it omits into spec
# from the server, as field manager jetstream-controller.
adoptionPolicy: AdoptOrCreate
name: REFUNDS
subjects: ["refunds.>"]
storage: File
replicas: 3
maxAge: 2160h
---
# adoptionPolicy defaults to Never, and a stream of this name already exists
# that the controller did not create: the resource goes Terminal and nothing
# on the server changes. See 01-status-natsstream-ledger.yaml.
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsStream
metadata:
name: ledger
namespace: payments
spec:
connectionRef:
name: shared-payments
# Hold (default): a Terminal condition stays until the resource is edited.
# Retry: the check reruns every resync period and clears itself once the
# cause is gone, for example when the conflicting stream is deleted by hand.
terminalPolicy: Retry
name: LEDGER
subjects: ["ledger.>"]
replicas: 3
kubectl apply -f https://nats-operator.io/docs/stories/03-unmanaged/01-natsstreams.yamlAfter adoption the payments resource carries the server’s config:
01-live-natsstream-payments.yaml
# The payments NatsStream as `kubectl get -o yaml` shows it after adoption:
# the controller wrote the server's config into spec.
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsStream
metadata:
name: payments
namespace: payments
spec:
connectionRef:
name: shared-payments
adoptionPolicy: Adopt
name: PAYMENTS
subjects: ["payments.>"]
storage: File
replicas: 3
retention: Limits
maxAge: 168h0m0s
maxBytes: "-1"
discard: Old
01-status-natsstream-payments.yaml
status:
observedGeneration: 2
conditions:
- type: Ready
status: "True"
reason: Synced
- type: Adopted
status: "True"
reason: FoundUnowned
message: adopted existing stream PAYMENTS; spec written from the server
lastTransitionTime: "2026-09-26T09:12:05Z"
- type: Synced
status: "True"
reason: MatchesSpec
lastSyncedTime: "2026-09-26T09:42:05Z"
ownership:
origin: Adopted
uid: 0f7c1a92-8e44-4a0b-b3a5-6c2de0f19b77
server:
leader: nats-1
messages: 3810442
bytes: 2Gi
A stream that exists and is not the controller’s, with no adoption policy set, stops the resource and leaves the server untouched. terminalPolicy: Retry rechecks it every resync period; the default Hold waits for an edit.
01-status-natsstream-ledger.yaml
status:
observedGeneration: 1
conditions:
- type: Ready
status: "False"
reason: Terminal
- type: Terminal
status: "True"
reason: ExistsUnowned
message: >-
stream LEDGER exists and carries no ownership marker; set
spec.adoptionPolicy to Adopt or AdoptOrCreate to take it over
lastTransitionTime: "2026-09-26T09:12:06Z"
# spec.terminalPolicy is Retry, so this is rechecked every resync period and
# clears once LEDGER is gone or carries the marker. Under Hold only an edit
# to the resource clears it.
nextCheckTime: "2026-09-26T09:22:06Z"
A key-value bucket and an object store #
Declared the same way as streams, with the same connection and lifecycle policies.
01-natskeyvalue-objectstore.yaml
# A key-value bucket and an object store, on the same connection as the
# streams above. Fields are nats.go's KeyValueConfig and ObjectStoreConfig in
# camelCase, with the bucket under `name`, defaulting to metadata.name. The
# lifecycle policies are the streams' own.
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsKeyValue
metadata:
name: sessions
namespace: payments
labels:
traffic: requests
spec:
connectionRef:
name: shared-payments
adoptionPolicy: AdoptOrCreate
deletionPolicy: Retain # the default for key-value buckets
name: sessions
history: 5
ttl: 24h
maxValueSize: 64Ki
maxBytes: 1Gi
storage: File
replicas: 3
---
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsObjectStore
metadata:
name: receipts
namespace: payments
spec:
connectionRef:
name: shared-payments
name: receipts
ttl: 2160h
maxBytes: 50Gi
storage: File
replicas: 3
compression: true
kubectl apply -f https://nats-operator.io/docs/stories/03-unmanaged/01-natskeyvalue-objectstore.yamlConsumers #
A consumer names its stream by server-side name, for a stream with no resource, or by reference to a NatsStream, which it then waits for.
01-natsconsumer.yaml
# A consumer names its stream one of two ways, never both.
#
# `stream`: the server-side name, for a stream that has no resource. The
# consumer's connection has to land in the stream's account.
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsConsumer
metadata:
name: ledger-audit
namespace: payments
spec:
connectionRef:
name: shared-payments
stream: LEDGER
name: audit
deliverPolicy: All
ackPolicy: Explicit
---
# `streamRef`: a NatsStream resource. The controller waits for it to be Ready
# before creating the consumer, and connectionRef defaults to the stream's.
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsConsumer
metadata:
name: payments-settlement
namespace: payments
spec:
streamRef:
name: payments
adoptionPolicy: AdoptOrCreate
# deletionPolicy omitted: Delete is the default for consumers.
name: settlement
deliverPolicy: All
ackPolicy: Explicit
ackWait: 30s
maxDeliver: 10
filterSubjects: ["payments.settled.>"]
kubectl apply -f https://nats-operator.io/docs/stories/03-unmanaged/01-natsconsumer.yaml