Pin the widget to a build you have tested
The widget is a third-party script on a page that handles real users. Pinning makes the browser load — and cryptographically verify — the exact bytes you tested, instead of whatever the server is serving today.
Two ways to load the widget
A plain install loads the floating build:
<script src="https://your-server.example/pointer.js" defer></script>
Unpinned, /pointer.js is whatever the server is serving today. The server marks
it no-cache so browsers revalidate, and every widget release replaces it in
place — your site picks up fixes without you touching anything. That auto-update behaviour
is convenient and is the right default for most installs.
A pinned install asks for one immutable build and verifies its bytes:
<script src="https://your-server.example/pointer.js?v=<hash>"
integrity="sha384-<hash>"
crossorigin="anonymous" defer></script>
The ?v= addresses one content-addressed build, the integrity
attribute is a real Subresource Integrity hash, and crossorigin="anonymous" is
required for the browser to check SRI on a cross-origin script. If the bytes do not match
the hash, the browser refuses to execute them.
What pointer init --pin writes
For plain static HTML installs, pointer init --pin asks the server which build
it is currently serving (its published /pointer.version.json manifest), then
writes the pinned block into your HTML with that build's hash and its published
sha384 integrity value:
<!-- pointer-feedback:start -->
<script src="https://your-server.example/pointer.js?v=9f1c2ab3" integrity="sha384-…" crossorigin="anonymous" defer></script>
<pointer-feedback project="my-project" server="https://your-server.example" environment="staging" source-attr="data-component-source"></pointer-feedback>
<!-- pointer-feedback:end -->
If the server's manifest cannot be reached or publishes no hash for
pointer.js, init exits with an error and tells you to re-run
without --pin — it never silently writes an unpinned tag you believe is pinned.
A Vite install pins too, though it looks different. That stack builds its script tag in JavaScript rather than writing markup, so the pin arrives as property assignments on the created element:
var s = document.createElement('script');
s.src = '%VITE_POINTER_SERVER%/pointer.js?v=<hash>';
s.integrity = 'sha384-…';
s.crossOrigin = 'anonymous';
Note the boundary: the /embed.js one-liner loader stays unpinned — it always
loads the current build. If you need a pinned install, use pointer init --pin
rather than the one-liner.
The trade-off, honestly
| Unpinned | Pinned | |
|---|---|---|
| Bytes served | Whatever the server ships today | The exact build you tested, browser-verified via SRI |
| Updates | Automatic, on the server's next deploy | Manual — you must re-run pointer init --pin to move to a new build |
| Caching | Revalidated every load (no-cache) |
immutable, cached for a year |
| Right for | Most sites — zero maintenance | Sites that must control exactly which third-party bytes run on their pages |
Unpinned auto-updates and is the right default for most sites. Pinning buys you exact, verified bytes — and the obligation to move the pin forward yourself. A pinned site that nobody maintains will eventually stop loading the widget at all (below).
What the server does with ?v=
-
?v=stable— the current build's bytes with a one-hour cache. A middle ground: current, but not revalidated on every load. -
?v=<retained hash>— an immutable, content-addressed build, served withCache-Control: public, max-age=31536000, immutable. -
Unknown, pruned, or malformed
v— a404, never a quiet fallback to the current bytes. Falling back would silently serve bytes that fail the pin's own SRI check; an explicit 404 is diagnosable.
When a pinned build is pruned
Previous builds are retained on the server (the widget build pipeline keeps the 10 newest
releases), so a pin keeps working across roughly ten widget releases. When your pinned
build finally falls out of the retained set, /pointer.js?v=<your hash>
starts returning 404 with a recovery hint in a response header:
HTTP/1.1 404 Not Found
X-Pointer-Widget-Version-Mismatch: <the server's current build hash>
The header names the build the server is currently serving, so a client can recover: read
it, re-run pointer init --pin (or update the snippet by hand), and the site is
pinned to the new build. The 404 carries Access-Control-Allow-Origin: * and
exposes the header to script precisely so a cross-origin page can read it — without that,
the browser would report an opaque network failure and the header would be unreadable by
the only kind of client that needs it. If the server itself could not load its version
manifest at startup, the header value is unknown instead.
Self-hosting behind a proxy: keep your cache rules off the pin
Self-hosters commonly put a no-cache rule in front of /pointer.js so the
floating build really does auto-update. That rule must exclude requests carrying a
v query: a pinned request is content-addressed and immutable, and a
proxy no-cache rule would overwrite the year-long immutable header the API chose. The bytes
would still be right — the pin still works — but the caching it exists to buy is destroyed
at the proxy, and nothing about the response looks wrong.
This is the matcher from this project's own Caddyfile:
@widget {
path /pointer.js /pointer.css /embed.js /skill.md /api/branding
not query v=*
}
header @widget >Cache-Control "no-cache, must-revalidate"
The not query v=* line is load-bearing. It lets both
?v=<hash> (immutable) and ?v=stable (one hour) keep the
cache headers the API set, while every unversioned widget URL is revalidated. Note that an
empty ?v= also matches query v=*, so it is excluded from the rule
too and falls through to the API — which answers it as an unknown hash: 404 plus the
version-mismatch header. An empty pin is a broken pin, not “no pin”.
Pinning means you decided a specific widget build is the one your site runs. Nothing
moves it forward except you — re-run pointer init --pin to adopt a newer
build (and commit the changed HTML), or drop the ?v=, integrity and
crossorigin attributes to go back to the floating build.