Configuration

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=

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”.

A pin is a commitment

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.