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.yaml

Adopting 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.yaml

After 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.yaml

Consumers #

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