Frontend SDK
Capture the encrypted fingerprint envelope in the browser — with a plain script tag, or the @signalgate/nextjs package for Next.js and React apps.
Quick Start
Protect your backend endpoints in minutes with the SignalGate plugin for Claude Code.
The plugin analyzes your repo, confirms placement with you, and writes the backend half of the integration — the SDK client wiring, two log() calls and a commented-out check() gate. Review-first: it shows every diff, never commits, and never reads your key. Detection runs on the events your backend forwards; the browser capture documented on this page is the other half, which you add by hand.
/plugin marketplace add SignalGate/signalgate-claude-plugin
/plugin install signalgate@signalgateThen, inside your backend repo:
/signalgate:integrateSupported backend stacks: Python 3.10+, Node.js 18+, Go 1.22+, Java 17+. Plugin source & README on GitHub
Once the backend half is in, add the browser capture below — start with Install.
Install
Add a single script tag to your page. The SDK loads once per session and is cached by the browser.
Browser-side library that collects device and browser signals and returns an encrypted four-field envelope. Zero runtime dependencies. Forward the envelope to your backend exactly as you received it.
<script src="https://sdk.signalgate.ai/v0.3.3/index.global.js"></script>Loaded once per session, cached by the browser. The IIFE bundle assigns to window.SignalGate for use by inline scripts.
Quick example
The canonical pattern: instantiate, start the SDK, and collect a payload on form submit.
<script src="https://sdk.signalgate.ai/v0.3.3/index.global.js"></script>
<script type="module">
const fp = new SignalGate.Fingerprint({
key: "YOUR_TENANT_KEY"
});
await fp.start(); // warm detectors once
const form = document.getElementById("login-form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const { payload } = await fp.get();
// payload = { encrypted, timestamp, nonce, v: 2 }
await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: form.email.value,
password: form.password.value,
signalgate: payload, // ← four-field envelope, treat as opaque
}),
});
});
</script>get() completes in ~200-500ms (detector warm-up). Subsequent calls within the cache window resolve in under a millisecond. Gated actions: for an action you gate with check() on your backend, capture TWO envelopes — call get() twice, one for the check and a fresh one for the post-success log — as shown in the Backend SDK live examples.Configuration
Options passed to the Fingerprint constructor.
| Option | Type | Default | Purpose |
|---|---|---|---|
| key | string | required | Your tenant's key, shown once in Public Keys settings. |
| cacheEnabled | boolean | true | Cache collected signals in sessionStorage so repeat calls are sub-millisecond. |
| cacheTTLMs | number | 1800000 | Cache lifetime (30 min by default). |
Payload shape
What fp.get() returns. The four fields are opaque to your code — pass them through to your backend unchanged.
interface EncryptedPayload {
encrypted: string; // base64 envelope, opaque
timestamp: number; // ms since epoch
nonce: string; // unique per call, server-deduplicated
v: 2; // envelope format version (the SDK emits v=2)
}
// fp.get() returns
interface FingerprintResult {
payload: EncryptedPayload;
raw?: FingerprintData; // only present when debug: true
}Your frontend code shouldn't parse, modify, or inspect the payload. The fields are designed for the backend to decrypt and evaluate.
Privacy and lifecycle
How the SDK behaves in your users' browsers.
- End-to-end encrypted.The fingerprint data is encrypted on the client before it leaves the browser. Your app server forwards an opaque blob; only SignalGate's backend can decrypt it.
- No third-party requests by default.The SDK doesn't phone home from the user's browser. The envelope leaves only when your code POSTs it to your own backend.
- Cache lives in sessionStorage (when
cacheEnabled) — cleared when the user closes the tab. No long-lived browser storage. - Degrades gracefully. The SDK degrades gracefully — if a browser blocks a specific capability, the rest of the signal payload still collects.
Install
One npm package. Peer dependency react >=18 <20; Node 18+. No next peer — plain React and Vite apps work too.
A thin "use client" React provider and hook for Next.js (App Router and Pages Router) and plain React or Vite apps. It is a runtime CDN loader, not a bundle: in the browser it injects the same pinned SDK artifact your page would otherwise embed by hand, and adds what a React integration has to hand-roll — SSR safety, warm-up timing, StrictMode idempotence and fail-open error handling.
getPayload() never throws and never rejects — it resolves null on every failure path (blocked CDN, timeout, an SDK error). Branch on null in every handler and decide your own policy: block, retry, or proceed without a signal.npm install @signalgate/nextjsCurrent release: @signalgate/nextjs 0.1.0. TypeScript types are bundled — no separate @types/ package. The fingerprint code itself stays on the CDN (browser SDK v0.3.3), loaded when the first provider mounts.
Provider setup
Wrap your app — or just the subtree that needs a payload — in SignalGateProvider. In the App Router that means importing it straight into your root layout: the client boundary is embedded in the package build.
// app/layout.tsx — a Server Component; the provider is the client boundary
// ("use client" ships inside the package build, so importing it here is fine).
import type { ReactNode } from "react";
import { SignalGateProvider } from "@signalgate/nextjs";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<SignalGateProvider
tenantKey={process.env.NEXT_PUBLIC_SIGNALGATE_KEY ?? ""}
>
{children}
</SignalGateProvider>
</body>
</html>
);
}window access, no injected script, no throw. Only the first provider to mount configures the load: script injection and warm-up are idempotent (one script, one instance, one start() per page load, StrictMode-safe); a second provider with different props logs a warning and is a no-op.The hook — capture on submit
useSignalGate() returns { getPayload, status, error }. Call getPayload() in your submit handler, branch on null, and forward the envelope to your own backend unchanged — the package never sends it anywhere itself.
"use client";
import { useSignalGate } from "@signalgate/nextjs";
export function CheckoutForm() {
const { getPayload, status } = useSignalGate();
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
// Never throws, never rejects — resolves null on any failure.
const payload = await getPayload();
if (payload === null) {
// Fail-open: no signal this time (blocked CDN, timeout, SDK error).
// Proceed, retry, or block — your policy; here we submit without it.
}
await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
item: new FormData(form).get("item"),
signalgate: payload, // ← four-field envelope, forward unchanged
}),
});
}
return (
<form onSubmit={onSubmit}>
{/* ...your fields... */}
<button type="submit" disabled={status === "loading"}>
Submit
</button>
</form>
);
}log() after the action succeeds, check() before an action you gate. Two captures for gated actions: the nonce is single-use per API call, so call getPayload() once for the check and again — a fresh capture — for the post-success log.Configuration
Props on SignalGateProvider.
| Option | Type | Default | Purpose |
|---|---|---|---|
| tenantKey | string | required | Your tenant's key from Public Keys settings, handed to the underlying browser SDK. |
| cdnUrl | string | pinned CDN URL | Override the script src — self-hosting, a staging mirror, or a local stub in tests. Omitted or falsy, the pinned default is used. |
| loadTimeoutMs | number | 10000 | Budget for the script load plus SDK warm-up before failing open. |
| scriptCrossOrigin | "anonymous" | "use-credentials" | false | "anonymous" | crossorigin on the injected script. false omits the attribute — for self-hosting on a host you cannot make send Access-Control-Allow-Origin. The trade: the browser mutes cross-origin error detail and Subresource Integrity is ruled out, so prefer setting the header when you can. |
| onError | (error: SignalGateLoadError) => void | — | Called at most once per provider with the fail-open reason. |
script-src 'self' https://sdk.signalgate.ai. A blocked script fails open — status "error", getPayload() resolves null — and nothing reaches the console unless you monitor CSP violation reports.Payload shape
The same four-field envelope as the script-tag SDK. Your code treats it as opaque and forwards it unchanged.
interface Payload {
encrypted: string; // base64 envelope, opaque
timestamp: number; // ms since epoch (producer clock)
nonce: string; // exactly 16 alphanumeric chars, single-use
v?: number; // envelope format version (the current SDK emits v: 2)
}
// useSignalGate() returns
interface UseSignalGateResult {
getPayload: () => Promise<Payload | null>; // null on every failure path
status: "idle" | "loading" | "ready" | "error";
error: SignalGateLoadError | null;
}getPayload() resolves this object or null — never a partial shape. Don't parse, modify, or inspect the fields; they are designed for the backend to decrypt and evaluate.
Runtime behavior
What the package does — and deliberately does not do — in your users' browsers.
- Runtime CDN loader, not a bundle. The package never vendors the fingerprint code into your build — the first provider mount injects the same pinned CDN artifact a plain script tag would (browser SDK v0.3.3). The CSP and asset story is identical to the script-tag route, and SDK upgrades are a SignalGate-side deploy, not an npm update your team has to ship.
- Fail-open everywhere. A blocked script, a timeout, or an SDK failure sets status to
"error", firesonErrorat most once, and makesgetPayload()resolve null. The antifraud layer can never take your UI down. - One load per page. Script injection and warm-up are module-level and idempotent — StrictMode double-mounts and any number of providers dedupe against one script element, one instance, one
start(). - Nothing leaves the browser by itself.The package sends no network requests with the envelope — it only hands you what the browser SDK sealed. POSTing it to your own backend is your integration's job.
Continue reading