Privacy engineering
What the browser script needs, what it stores and when, and what it sends.
Content Security Policy
The script talks only to its own origin. Add these sources to your policy:
script-src https://nolgard.com;
connect-src https://nolgard.com;
worker-src blob:;connect-src: the script calls/v1/challengeand/v1/collect, and/v1/stepup/*or/v1/appealonly if you callstepUp()orappeal(). All requests send no cookies.worker-src blob:: one short-lived worker built from a blob URL reads browser and GPU details. If your policy blocks it, collection still works without those signals.- No
style-src,img-srcorframe-srcentry is needed. The script sets styles through the DOM style API, draws on canvases it never loads images into, and uses a hidden iframe with nosrc. It does not useeval.
Nonces. With a nonce-based policy, put your nonce on the tag: <script nonce="..." src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer>. With 'strict-dynamic', host sources are ignored, so the nonce is required; a script you insert from a nonced script is trusted too.
When the client writes storage
Measured in Chrome on 25 October 2026:
| Moment | Cookie | localStorage | IndexedDB |
|---|---|---|---|
Script loaded, Nolgard.ready resolved | none | none | none |
Page idle, no execute() | none | none | none |
After the first execute() or gate() | __nf_vid | __nf_vid | database __nf, store v |
The value is one random visitor id, the same in all three places. The cookie is first-party on your domain, Path=/, SameSite=Lax, Secure on HTTPS, 400 days, renewed when used. A missing copy is restored from the others. sessionStorage is never used. The id lets us recognize the same browser at the next signup after the user clears one store or changes email.
No-storage mode
Add data-storage="none" to the script tag:
<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" data-storage="none" defer></script>The script then reads and writes no cookie, localStorage or IndexedDB, and sends no visitor id. Linking falls back to the device fingerprint and the email. Repeat signups are caught less often: a user who comes back in the same browser with a new email links only if the fingerprint matches.
Pinning the script with SRI
/v1/client.js changes with each release and is cached for 5 minutes. To pin it, read GET https://nolgard.com/v1/client-manifest.json. It returns {"url":"/v1/client.<hash>.js","integrity":"sha384-..."}. The hashed file is served with immutable caching and CORS, so <script src="https://nolgard.com/v1/client.<hash>.js" integrity="sha384-..." crossorigin="anonymous"> works.
A pinned build keeps loading, but verify only accepts tokens from the current and the previous release. Refresh the pinned URL and hash from the manifest on every Nolgard release, or keep using /v1/client.js.
Consent and ePrivacy
Article 5(3) of the ePrivacy Directive covers storing and reading information on the user's device. In default mode the script stores the visitor id above, only when you call execute() or gate(), for fraud prevention on that form. Whether that needs consent on your site is your DPO's decision, not ours. If the answer is yes and you cannot wait for consent, use data-storage="none". If you collect consent first, load the script or call gate() after it.
Global Privacy Control
The script does not change its behaviour when the browser sends GPC. GPC is a signal to opt out of the sale or sharing of personal data. We do not sell visitor data and do not use it for advertising. A farm could also switch GPC on to remove the visitor id. If you want to honour GPC with less storage, read navigator.globalPrivacyControl and add data-storage="none" when it is true.
Load on form focus
To keep the script off pages and visitors that never start a signup, insert it when the form gets focus:
const form = document.querySelector('#signup');
form.addEventListener('focusin', () => {
const s = document.createElement('script');
s.src = 'https://nolgard.com/v1/client.js';
s.dataset.siteKey = 'SITE_KEY';
document.head.append(s);
}, { once: true });The script makes one /v1/challenge request when it loads and starts recording interaction timing from that moment, so focus is the latest point to load it. Before submit, wait for window.Nolgard to exist, then call execute(). If it never appears, send the form without a token and follow the missing-token path below.
Single-page apps
Load the script once per page load. It survives route changes, and window.Nolgard stays the same object. Call execute() at each submit, not at route change: a token is single-use and expires after 5 minutes. Do not insert the script again on every route.
Ad blockers and Tor
Ad blockers, Brave Shields and Tor Browser at the Safest level can block the script. The form then has no token, and verify returns missing-token. That is not a bot verdict. Handle it as described in the error table: email confirmation, then a reduced grant.
Page URLs and referrers
On each execute() or gate(), the script sends the full page URL (location.href, path and query) and document.referrer, each up to 2,048 characters. They are stored with the event and deleted with it, see retention.
On sites where a URL can reveal health, sexuality, religion or other special category data (GDPR Article 9), keep that out of the URL where you call execute():
- Run the check on a neutral signup URL, not on a URL with a profile, condition or search term in it.
- Remove sensitive query parameters with
history.replaceStatebefore callingexecute(). - Keep your
Referrer-Policyatstrict-origin-when-cross-originor stricter, so links from other sites arrive as an origin only. Same-site referrers still carry the full path. - Never put emails or other personal data in action names or URLs.