Guides / Static assets

Uploading and serving assets

Anvil is an asset-hosting platform: you push immutable files into a bucket and the edge serves them using the cache headers you declare. This page covers a first upload, then the reference material you need once the bucket is live.

Overview

Every bucket is addressed by a hostname and holds a flat namespace of objects. Objects are content-addressed on write: an upload computes a digest, stores the bytes once, and attaches the path you asked for as a pointer to them. Re-uploading identical bytes under a second path is almost free, and a rollback just repoints a path at an earlier digest.

There is no build step and no server-side rendering: the edge matches a request path to a pointer, applies the headers from your configuration, and streams the object.

Install the tool

The command line client is a single static binary with no runtime dependencies. Place it anywhere on your PATH and confirm that it runs. Credentials are read from the environment on each call, so nothing is written to your home directory.

Upload a file

An upload is one call: name a local file, give it the path you want inside the bucket, and the client streams the bytes and prints the digest. Uploads are atomic: a path points either at the new object or the previous one, never at a half-written body.

$ anvil put ./dist/app.4f21c9.js assets/app.4f21c9.js
  uploaded  18.4 kB  sha256:9c1f…a07d  (new object)
  serving   /assets/app.4f21c9.js

Verify the response

Request the path back over HTTPS and read the headers. A healthy response carries the digest in an ETag, the age of the cached copy, and the identifier of the edge location that answered. If the age header is missing, the request reached the origin directly and your rules are not in effect yet.

Note

Pointer changes reach edge locations asynchronously: a path with cached copies in the field may keep answering with the previous object for up to sixty seconds after an upload. This is why filenames should carry a content hash: with immutable names you never wait for a purge.

Cache control

Anvil never guesses a policy. Each rule matches a path prefix and sets the response headers verbatim, so what the browser sees is what you wrote. Rules are evaluated most specific prefix first and the first match wins; a path with no matching rule falls back to a short revalidated lifetime.

Two lifetimes matter: the browser lifetime, which you cannot recall once served, and the edge lifetime, which you can invalidate at any time. Keep the first long only for hashed filenames.

Common values by asset class
Asset classCache-Control
Hashed bundlespublic, max-age=31536000, immutable
Fonts and iconspublic, max-age=2592000
Entry documentspublic, max-age=0, must-revalidate
Generated feedspublic, max-age=300, stale-while-revalidate=60

Configuration file

Rules live in one declarative file at the root of the bucket. It is read on publish and validated as a whole: if any rule is malformed the file is rejected, so there is no partial apply.

[bucket]
name    = "site-assets"
default = "public, max-age=60"

[[rule]]
prefix        = "/assets/"
cache_control = "public, max-age=31536000, immutable"
compress      = ["br", "gzip"]

[[rule]]
prefix        = "/index.html"
cache_control = "public, max-age=0, must-revalidate"

Path rewriting

A rule may rewrite a request path before the pointer lookup. Rewrites are literal prefix swaps, not regular expressions, and apply once: a rewritten path is never fed back through the rule list. Use them to keep an old prefix alive without duplicating bytes.

Limits

A single object may be up to five gigabytes; larger payloads must be split by the producer. A bucket holds one million pointers, and a configuration file may declare two hundred rules. Uploads are rate limited per credential, not per bucket.