nats — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited nats (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Docs: https://docs.nats.io | Examples: https://natsbyexample.com
NATS is a subject-based connective layer for distributed systems. Messages route by subject string, not hostname:port. Server binary is ~20MB, runs on Raspberry Pi to cloud. CNCF project, Apache 2.0 licensed, 40+ client libraries. Max message payload: 1MB default (configurable up to 64MB; keep under ~8MB in practice).
Two planes — choose before designing:
| Plane | Delivery | Persistence | Use for |
|---|---|---|---|
| Core NATS | At-most-once | None | Fire-and-forget, RPC, real-time fan-out |
| JetStream | At-least/exactly-once | Streams | Durable queues, replay, KV state, work queues |
Deep reference files in references/:
jetstream.md — full StreamConfig/ConsumerConfig fields, async publish, KV/object-store opsserver-deployment.md — full config options, Docker, Kubernetes, Go embedding structsecurity.md — NKey, JWT/operator model, nsc CLI, TLS, auth callout→ https://docs.nats.io/nats-concepts/subjects
service.orders.created # exact (publishers always use exact subjects)
service.orders.* # * = one token (subscribers only)
service.orders.> # > = one or more tokens at end (subscribers only)-, _ only (avoid other special chars)$ prefix reserved for system use ($SYS.*, $JS.*, $KV.*, $SRV.*)→ https://docs.nats.io/nats-concepts/core-nats
Fan-out to all subscribers. Zero config — subjects are ephemeral.
nc.Publish("orders.created", data)
nc.Subscribe("orders.*", func(msg *nats.Msg) { /* handle */ })→ https://docs.nats.io/nats-concepts/core-nats/pubsub
Requester sends to a subject with a temp reply-to inbox (_INBOX.<nonce>). First responder wins.
msg, err := nc.Request("svc.lookup", payload, 2*time.Second)
// err == nats.ErrNoResponders when no subscriber (immediate 503)nc.Subscribe("svc.lookup", func(msg *nats.Msg) { msg.Respond(result) })→ https://docs.nats.io/nats-concepts/core-nats/reqreply
Competitive consumers — one random member gets each message. Scale horizontally with zero reconfiguration. Geo-affinity: local consumers served first.
nc.QueueSubscribe("orders.created", "order-processors", handler)→ https://docs.nats.io/nats-concepts/core-nats/queue
→ https://docs.nats.io/nats-concepts/jetstream
Enable on server: add jetstream {} block or pass --jetstream flag.
Streams persistently capture Core NATS subjects. Configuration is separate from consumption.
js, _ := nc.JetStream()
js.AddStream(&nats.StreamConfig{
Name: "ORDERS",
Subjects: []string{"orders.>"},
Storage: nats.FileStorage, // or MemoryStorage
Replicas: 3, // clustered only; 1, 2, 3, or 5
Retention: nats.LimitsPolicy, // default
MaxAge: 24 * time.Hour,
MaxBytes: 1 << 30,
})Retention policies (see references/jetstream.md for decision tree):
| Policy | Behavior |
|---|---|
LimitsPolicy | Retain until age/size/count limits hit (default) |
WorkQueuePolicy | Delete on ack; one consumer per subject |
InterestPolicy | Retain while consumers have unread messages |
→ https://docs.nats.io/nats-concepts/jetstream/streams
Always use `js.Publish()` — not nc.Publish() — to receive server ack confirming storage.
ack, err := js.Publish("orders.created", data)
// ack.Stream, ack.Sequence confirm exactly where it was stored
// Exactly-once: include Nats-Msg-Id header (dedup window: 2 min default)
js.PublishMsg(&nats.Msg{
Subject: "orders.created",
Header: nats.Header{"Nats-Msg-Id": []string{uniqueID}},
Data: data,
})Prefer pull consumers for new projects. Use push only for ordered replay with a single subscriber.
// Durable pull consumer
js.AddConsumer("ORDERS", &nats.ConsumerConfig{
Durable: "order-worker",
FilterSubject: "orders.created",
AckPolicy: nats.AckExplicitPolicy,
DeliverPolicy: nats.DeliverAllPolicy,
MaxDeliver: 5,
AckWait: 30 * time.Second,
})
sub, _ := js.PullSubscribe("orders.created", "order-worker")
msgs, _ := sub.Fetch(10, nats.MaxWait(5*time.Second))
for _, msg := range msgs {
msg.Ack() // or .Nak(), .InProgress(), .Term()
}// Ordered push consumer (single subscriber, no ack, for replay/inspection)
sub, _ := js.SubscribeSync("orders.>", nats.OrderedConsumer())| Consumer type | When to use |
|---|---|
| Pull, durable | Scaled workers, batching, explicit flow control |
| Pull, ephemeral | Short-lived processing without persistence |
| Push, ordered | Sequential replay, data inspection (single subscriber) |
| Push, durable | Legacy; avoid for new work |
Delivery policies: DeliverAllPolicy · DeliverLastPolicy · DeliverLastPerSubjectPolicy · DeliverNewPolicy · DeliverByStartSequencePolicy · DeliverByStartTimePolicy
Ack policies: AckExplicitPolicy (default) · AckNonePolicy · AckAllPolicy
→ https://docs.nats.io/nats-concepts/jetstream/consumers
Full consumer config fields → references/jetstream.md
Built on JetStream streams (prefix KV_). Immediately consistent; no read-your-writes guarantee on direct gets (use Watch for consistency). Valid key chars: alphanumeric + _, -, ., =, /.
kv, _ := js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: "config",
TTL: 1 * time.Hour,
History: 5, // keep last 5 revisions per key (default: 1)
})
kv.Put("flags.dark-mode", data)
entry, _ := kv.Get("flags.dark-mode") // entry.Value(), .Revision()
kv.Delete("flags.dark-mode")
kv.Create("lock", data) // compare-to-null-and-set; fails if exists
kv.Update("lock", newData, revision) // CAS
watcher, _ := kv.Watch("flags.*")
for entry := range watcher.Updates() { /* nil = end of initial snapshot */ }→ https://docs.nats.io/nats-concepts/jetstream/key-value-store
Full KV API → references/jetstream.md
Chunked file storage on JetStream. Not a distributed filesystem — all objects must fit on the target node.
obs, _ := js.CreateObjectStore(&nats.ObjectStoreConfig{Bucket: "artifacts"})
obs.PutFile("model.bin", "/local/path/model.bin")
obs.GetFile("model.bin", "/dest/model.bin")
obs.Delete("model.bin")
watcher, _ := obs.Watch()→ https://docs.nats.io/nats-concepts/jetstream/obj_store
→ https://docs.nats.io/using-nats/developer/connecting
nc, err := nats.Connect(
"nats://s1:4222,nats://s2:4222", // comma-separated cluster seeds
nats.UserCredentials("app.creds"), // JWT+NKey creds file
// nats.Token("secret") // or token
// nats.NkeyOptionFromSeed("user.nk")
nats.MaxReconnects(-1), // -1 = infinite (default: 60 attempts)
nats.ReconnectWait(2*time.Second),
nats.ReconnectJitter(100*time.Millisecond, time.Second),
nats.ReconnectBufSize(8<<20), // 8MB buffer during reconnect
nats.DisconnectErrHandler(onDisconnect),
nats.ReconnectHandler(onReconnect),
nats.ClosedHandler(onClose),
nats.ErrorHandler(onAsyncError),
)
defer nc.Drain() // flush pending, then close — never use nc.Close() in productionURL schemes: nats:// (opportunistic TLS) · tls:// (mandatory TLS) · ws:// · wss://
→ https://docs.nats.io/using-nats/developer/connecting/reconnect
→ https://docs.nats.io/nats-concepts/security
| Method | Complexity | Notes |
|---|---|---|
| Token | Low | Single shared secret |
| Username/Password | Low | Use bcrypt hashes in server config |
| NKey | Medium | Ed25519; private key never leaves client |
| JWT + NKey | High | Decentralized; new users without server restart |
| TLS client cert | Medium | Cert CN/SAN maps to user |
| Auth callout | High | Delegate auth to an external NATS service |
Subject-level permissions per user:
users: [{ user: svc, password: "...",
permissions: { publish: ["events.>"], subscribe: ["_INBOX.>"] } }]Accounts provide isolated namespaces (multi-tenancy). JetStream resources are per-account scoped.
Full NKey, JWT/operator setup, nsc CLI, TLS config → references/security.md
→ https://docs.nats.io/running-a-nats-service/configuration
server_name: node-1
port: 4222
http: localhost:8222 # monitoring — bind localhost only; no built-in auth
jetstream {
store_dir: "/data/nats" # each cluster node needs its own directory
max_memory_store: 2GB
max_file_store: 100GB
}
cluster {
name: my-cluster
listen: 0.0.0.0:6222
routes: ["nats://node-2:6222", "nats://node-3:6222"]
}Hot reload: nats-server --signal reload
→ https://docs.nats.io/running-a-nats-service/configuration/clustering/jetstream_clustering
server_name and its own store_dirserver_tags + stream Placement to control replica distribution across zones→ https://docs.nats.io/running-a-nats-service/configuration/leafnodes
Lightweight hub-spoke extension. Local clients authenticate locally; traffic bridges to hub only as needed. Best for edge/IoT, air-gapped sites, third-party tenant clusters.
leafnodes {
remotes: [{ url: "nats-leaf://hub.example.com:7422",
credentials: "/etc/nats/edge.creds" }]
}import server "github.com/nats-io/nats-server/v2/server"
opts := &server.Options{
ServerName: "embedded", Host: "127.0.0.1", Port: 4222,
JetStream: true, StoreDir: "/tmp/nats",
}
s, _ := server.NewServer(opts)
s.ConfigureLogger()
go s.Start()
if !s.ReadyForConnections(5 * time.Second) { panic("not ready") }
defer s.Shutdown()
nc, _ := nats.Connect(s.ClientURL())Full server.Options struct, Docker, Kubernetes → references/server-deployment.md
→ https://docs.nats.io/using-nats/developer/services
Build discoverable services over Core NATS — no special server support needed.
import "github.com/nats-io/nats.go/micro"
svc, _ := micro.AddService(nc, micro.Config{Name: "orders", Version: "1.0.0"})
svc.AddEndpoint("create", micro.HandlerFunc(func(req micro.Request) {
req.Respond(result)
}))
// Endpoints are addressable at "orders.create" by defaultAuto-handled discovery subjects: $SRV.PING.> · $SRV.INFO.> · $SRV.STATS.>
→ https://docs.nats.io/nats-concepts/subject_mapping
Applied server-side before routing — transparent to publishers and consumers.
# Canary deployment: 2% to v2
mappings: {
"orders.>" : [
{ destination: "orders.v1.{{wildcard(1)}}", weight: 98 }
{ destination: "orders.v2.{{wildcard(1)}}", weight: 2 }
]
}
# Deterministic partitioning (preserves per-key order)
mappings: {
"orders.>" : "orders.shard.{{partition(4,1)}}.{{wildcard(1)}}"
}Transform functions: {{wildcard(n)}}, {{partition(n,idx)}}, {{split(n,sep)}}, {{SliceFromLeft(n,count)}}, {{SliceFromRight(n,count)}}.
Scopes: root config (default account), per-account, imported subjects, per-stream (via SubjectTransform in StreamConfig).
→ https://docs.nats.io/running-a-nats-service/nats_admin/monitoring
HTTP on port 8222. No authentication — bind to localhost only.
| Endpoint | Returns |
|---|---|
/varz | Server state, uptime, memory |
/connz | Connections, throughput, RTT |
/jsz | JetStream streams, consumers, lag |
/healthz?js-enabled=1 | Readiness including JetStream |
/subsz | Subscription routing table |
/leafz | Leaf node connections |
Integrations: nats-top (live CLI), Prometheus exporter, Grafana dashboards.
→ https://docs.nats.io/using-nats/nats-tools/nats_cli
# Context
nats context add prod --server nats://prod:4222 --creds prod.creds
nats context select prod
# Core
nats pub orders.created '{"id":"1"}'
nats sub "orders.>"
nats request svc.lookup '{"q":"foo"}'
nats reply svc.lookup '{"result":"bar"}'
# Streams
nats stream add ORDERS --subjects "orders.>" --storage file --replicas 1
nats stream ls / info ORDERS / rm ORDERS
nats stream purge ORDERS
# Consumers
nats consumer add ORDERS worker --pull --deliver all --ack explicit
nats consumer next ORDERS worker --count 10
nats consumer info ORDERS worker
# KV
nats kv add config && nats kv put config key val
nats kv get config key && nats kv watch config
# Benchmarks & diagnostics
nats bench orders.bench --pub 4 --sub 4 --msgs 1000000
nats server info && nats server ping && nats rtt| Mistake | Fix |
|---|---|
nc.Publish() to a JetStream subject | Use js.Publish() — only way to get storage ack |
nc.Close() on shutdown | Use nc.Drain() — flushes buffered messages |
| Push consumer with horizontal scaling | Use pull consumer — push is single-subscriber |
| Two overlapping subscriptions on one conn | Both receive the message — deduplicate intentionally |
Missing nc.Flush() after Subscribe() | Subscription is buffered — call nc.Flush() before relying on it |
Multiple WorkQueuePolicy consumers per subject | Server enforces one consumer per subject |
Monitoring port on 0.0.0.0 | No auth exists — bind to localhost |
Ignoring ErrNoResponders on Request() | Handle 503; no active subscriber on that subject |
Shared store_dir across cluster nodes | Each JetStream node must have its own storage dir |
| MQTT client without JetStream enabled | MQTT requires JetStream for session + retained messages |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.