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 urlServer-side clamping
| Field | Behaviour |
|---|---|
| expires_in = 0 | Use signed_url_default_ttl from config |
| expires_in > max | Clamped to signed_url_max_ttl |
| expires_in <= 0 | Rejected (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.