Serve static files straight out of a NATS JetStream object store over HTTP.
nats-static opens a single object store bucket and:
GET/HEADany key over HTTP. The request path maps to a flat object key (cleaned, leading slash stripped; an empty path servesindex.html). The responseContent-Typecomes from the object's storedcontent-typemetadata, falling back to the file extension, thenapplication/octet-stream.ETag(object digest) andLast-Modifiedare set, and conditional requests (If-None-Match,If-Modified-Since) return304;Rangerequests return206. Writers can attach extra response headers — see Writing objects.static.put.obj(NATS request): store an object in one message. Headers:Path(required) and optionalContent-Type; the body is the content. RepliesOK size=… digest=….static.put.begin(NATS request): open a multipart session for objects larger than the NATS max payload — see Writing objects.static.delete(NATS request): remove an object byPath. RepliesOK.
Writes can be scoped to a path prefix by encoding it in the subject (static.put.obj.blog,
static.put.begin.blog, static.delete.blog, …) — see Writing objects.
The object store bucket is never created by the server — it is provisioned out of band (by
NACK, the chart's nack.enabled ObjectStore CRD, or any other
means). On boot the server retries opening the bucket until it exists.
Config is a JSON file; its path comes from NATS_STATIC_CONFIG (default
/etc/nats-static/config.json):
{
"nats": {
"url": "nats://nats:4222",
"user_file": "...",
"password_file": "..."
},
"object_store": { "bucket": "static" },
"http": { "listen": ":8080" }
}NATS auth is selected by whichever credential file is set: user_file+password_file,
token_file, creds_file, or nkey_seed_file. With none set the connection is anonymous. The
files hold the credential material (mounted from a Kubernetes Secret by the chart).
nats-static version prints the version and exits.
A chart lives in charts/nats-static. It renders the Deployment, Service,
ConfigMap, and (optionally, via nack.enabled) the NACK ObjectStore and Account CRDs. It does
not render an Ingress — point your own Ingress at the Service's http port. See
values.yaml for the auth/secret and NACK options.
Prebuilt images are published to ghcr.io/josh/nats-static.
Write verbs live under static.put.* plus static.delete. Each is addressable bare
(unrestricted) or prefix-scoped by appending the prefix as subject tokens (. → /) — e.g.
static.put.obj.blog.images confines writes to blog/images/. The server rejects any Path
outside the subject's prefix (ERR path outside permitted prefix).
Single-shot — static.put.obj[.<prefix>]. One request carries the whole object (≤ NATS
max_payload, 1 MiB default). Headers: Path (required), optional Content-Type. Replies
OK size=… digest=….
Multipart — static.put.begin[.<prefix>]. For larger objects, a request/reply protocol streams
chunks into the store with bounded memory:
static.put.beginwithPath(optionalContent-Type,Size,Digest) → replyOK session=<id> subject=static.put.session.<id> max_chunk=<bytes> mode=acked. The session is pinned to the replica that answered.- Send chunks to the session subject with a monotonic
Seq(from0, body ≤max_chunk); each is ackedOK seq=<n>. Any gap/duplicate/out-of-orderSeqaborts the upload. - Finish with an
EOF: truerequest (empty body) →OK size=<n> digest=<SHA-256=…>. ASize/Digestdeclared at begin that doesn't match rolls the object back with anERR.
Response headers. Uploads may carry an allowlist — Cache-Control, Content-Disposition,
Content-Encoding, Content-Language, Vary (plus Content-Type) — stored on the object and
replayed on GET/HEAD. Other headers are ignored.
Prefix scoping. Because the boundary is in the subject, a writer can be delegated a path
namespace via NATS export/import — grant static.put.obj.<prefix> (and .>),
static.put.begin.<prefix> (and .>), and static.put.session.>, and it can write only under that
prefix. Bare static.put.* stays unrestricted; keep static.delete unexported to make a writer's
access append-only.