Signed URLs

A signed URL is a self-contained, time-boxed share link. The token embeds the bucket, path, scope (read or write), expiry, and the bucket's signing_version, all sealed with an HMAC-SHA256 over the server's signing key.

Anatomy

http://host:7474/storage/v1/object/signed/<token>

token = base64url(payload) "." base64url(hmac_sha256(key, payload))

payload = {
  "bucket": "avatars",
  "path": "alice.png",
  "exp": 1778838131,           // Unix seconds
  "iat": 1778834531,
  "scope": "read" | "write",
  "bucket_version": 0,
  "transform": { ... }          // optional, render endpoints only
}

Mint a token

# REST - needs an authenticated session that can SELECT the object.
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expires_in": 3600}' \
  http://localhost:7474/storage/v1/object/sign/avatars/alice.png

# CLI
anvil storage sign avatars/alice.png --ttl 1h

# Cypher
RETURN signed_url(o, 3600) AS url

Server-side clamping

FieldBehaviour
expires_in = 0Use signed_url_default_ttl from config
expires_in > maxClamped to signed_url_max_ttl
expires_in <= 0Rejected (400)

Revocation

The token embeds the bucket's current signing_version. Bump it to invalidate every outstanding token for that bucket:

# REST
curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://localhost:7474/storage/v1/bucket/avatars/sign-revoke

# Hammer UI: Storage → bucket → Settings → "Revoke signed URLs"

Existing valid signatures still fail the version check after a revoke. The mint endpoint always uses the latest version, so newly minted URLs after the bump work normally.

Upload signing

Browser-side uploads to private buckets typically use a write-scoped signed URL so the browser doesn't need to hold a bearer token. POST /storage/v1/object/upload/sign/... returns a PUT-able URL.