signed mode requires every serving URL to carry a signature. This page is the whole specification — enough to reimplement in any language.
Signing is opt-in. A new Theme is
public, and a public Theme ignores a signature if one is present. Turn signing on in the portal, on the Theme’s Settings tab.Keys
A Theme holds a set of signing keys. Each key is a pair:
A Theme can hold several keys at once, which is what makes rotation possible without downtime.
The algorithm
path is the URL path from the leading slash, with no host and no query string:
Three details that matter
The key id is inside the signed message
The key id is inside the signed message
?kid= is part of the canonical string, not just of the URL. A signature made with one key therefore cannot be replayed against another key’s id on the same path.The canonical path is Livry's spelling, not the request's raw path
The canonical path is Livry's spelling, not the request's raw path
A URL with a doubled slash, a percent-encoded letter or a trailing dot is normalised before the signature is checked. You cannot sign one spelling and serve another.
There is no host in the signed message, and no expiry
There is no host in the signed message, and no expiry
No host, so the same signature keeps working if
cdn.livry.dev moves behind a CDN.No expiry, deliberately. An expiring signature changes the URL on every mint, which would defeat an edge cache entirely — and caching is the whole premise of the serving path. Deleting a key is the revocation mechanism; see below.Worked example
base64url with no padding.
- and _ rather than + and /, and trailing = stripped. Node’s "base64url" and .NET’s Base64Url do this for you; Python and shell need the transform shown above.Where to sign
Because there is no expiry, a signed URL is stable — you can compute it once and cache it alongside anything else.
Verification, and what a failure looks like
A signed Theme answers403 when the signature is missing, malformed, made with an unknown key, or simply wrong. It is one answer for all four, so a caller probing keys learns nothing about which key ids exist.
The comparison is constant-time, over the decoded bytes.
Rotation
A Theme holds several keys at once, so rotation has no cutover:1
Add a second key
Both are now valid. URLs signed with either are served.
2
Deploy your app signing with the new key
Existing URLs signed with the old one keep working.
3
Delete the old key
URLs signed with it start answering
403.Signing is not an allowlist
A signature proves the URL was minted by someone holding your secret. It says nothing about where the request came from — anyone who obtains a signed URL can fetch it. If you also need to constrain who may fetch, that is a separate mechanism:Access control
Origin allowlists, IP ranges, and what each one is actually worth.