Matching the same device across Chrome, Safari, Firefox, and Brave is usually framed as a server-side correlation problem. Partition the signal set by what varies between browsers and the cross-browser identifier becomes deterministic and computable client-side. The hard part is the partition, not the match.
The assumption: cross-browser matching is a server problem
Ask how to recognise the same physical device across different browsers and the standard answer involves a backend. Collect signals in the browser, ship them to a service, and run probabilistic matching against a database of prior visitors: identity graphs, IP clustering, behavioural correlation, a model that outputs a confidence score. The matching lives on the server because the assumption is that no single client-side value is stable enough to key on across browsers.
That assumption is half right. Most fingerprinting libraries return one hash per browser, and that hash genuinely does change between Chrome and Safari on the same machine, because it folds in signals that are properties of the browser engine, not the device. If your only identifier moves when the browser moves, then yes, you need a correlation engine to stitch the pieces back together.
But the server is solving a problem the client created by hashing the wrong set of signals. Partition the set correctly and most of the correlation engine disappears.
The reframe: the signal set splits cleanly
Every browser fingerprint is a bundle of signals, and those signals fall into two groups that behave very differently across browsers on one machine.
Engine-bound signals are properties of how a specific browser processes content: the exact output of a canvas rasteriser, the numeric profile of an audio pipeline, the way a rendering engine reports certain capabilities. Open the same machine in Chrome and in Firefox and these differ, by design, because the engines differ. They are excellent at telling two browsers apart and useless at telling two machines apart.
Hardware-bound signals are properties of the physical device underneath the browser: the GPU's reported identity, the CPU architecture, the set of installed fonts, the screen geometry, and similar device-level facts. Every browser on one machine reads these the same way, because they are reading the same hardware. They are the opposite tool: strong at identifying a machine, weak at distinguishing browsers.
Once you see the bundle this way, the cross-browser question answers itself. You do not need a model to correlate browsers; you need to stop mixing the two groups in the first place.
Why the split makes matching deterministic
Hash only the hardware-bound subset and the result is identical in Chrome, Safari, Firefox, and Brave on the same machine. Not similar, identical. That single property is what removes the server.
Probabilistic matching exists because similarity is expensive to decide: you need a scoring function, a threshold, and somewhere to run them. Equality is cheap to decide. If the cross-browser identifier is a deterministic hash of an invariant signal subset, then 'is this the same device' is a string comparison, and a string comparison needs no backend, no database scan, and no model.
This is the difference between the two outputs Benny returns from a single getFingerprint call. fingerprint folds in engine-bound signals and is browser-specific by design, the right key for 'same browser, returning session'. hardwareFingerprint is the deterministic hash of the hardware-bound subset, the right key for 'same device, any browser'. The cross-browser match is that second hash, compared as equality.
import { getFingerprint, compareFingerprints } from 'doorman-benny';
// Both identifiers come from one pass. No network call computes them.
const a = await getFingerprint(); // e.g. collected in Chrome
// ... later, on the same machine in Safari:
const b = await getFingerprint();
// Engine-bound: these differ across browsers, by design.
a.fingerprint === b.fingerprint; // false across Chrome / Safari
// Hardware-bound: deterministic across browsers on one machine.
a.hardwareFingerprint === b.hardwareFingerprint; // true
// The cross-browser verdict is a local, synchronous comparison.
// No server, no database, no probabilistic model.
const cmp = compareFingerprints(a, b, { mode: 'cross-browser' });
cmp.hardwareMatch; // trueThe cross-browser match is a deterministic equality check, with an optional fuzzy comparison, both computed locally. Nothing here makes a network call.
The hard part is drawing the line
If the partition were obvious, every library would ship it. It is not. Plenty of signals look hardware-bound but quietly leak engine specifics, and a single mislabelled signal poisons the invariant: include one engine-bound value in the hardware hash and the hash stops being cross-browser stable. The engineering is not the hashing, which is trivial. It is deciding, signal by signal, which side of the line each one sits on, and proving the hardware subset actually holds across engines.
Robust degradation is part of the same problem. A signal can be unavailable in one browser and present in another, and a naive implementation would let that difference change the hash. Each signal in Benny carries an explicit outcome rather than a bare value: a SignalResult records whether the signal was collected, or whether it was unsupported, threw, timed out, or was randomised by the browser. The hash is built so that a signal absent for a structural reason degrades predictably instead of forking the identity between browsers.
What still needs care, and what still needs a server
Two honest caveats. First, hardware-bound does not mean immutable. Legitimate events shift these signals: an OS upgrade, an external monitor, a GPU driver change. Strict hash equality is the right tool for most of the cross-browser case, but a tolerant path matters for returning users months later. That is why compareFingerprints ships more than one mode: cross-browser for the hardware-only equality case, and graded modes that apply per-signal fuzzy matching when you expect drift.
Second, anti-fingerprinting browsers deliberately break the invariant by injecting noise, and treating their output naively would either split one device into many identities or collapse many into one. Brave-aware handling resolves the known cases at a category level so a privacy browser does not read as a new device on every visit.
And the honest limit of the 'without a server' claim: you do not need a server to compute the identity or to decide a match, but you still need somewhere to keep prior fingerprints to compare against. The point is what that store is. It is a key-value lookup keyed on a deterministic hash, not a correlation engine running probabilistic matching. The intelligence moved from the backend into the partition.
Why this is the interesting part
The reflexive architecture for cross-browser identity is a data-collection client plus a server-side matching brain. Partitioning the signal set by hardware versus engine inverts that: the matching becomes a local equality check, and the brain becomes the one-time, careful decision of where each signal belongs.
The server you were going to build was compensating for a hash that mixed two incompatible kinds of signal. Separate them and most of that server was never necessary.
Frequently asked questions
Does cross-browser matching really work with no network call?
The identifiers and the comparison are computed entirely in the browser. getFingerprint produces both the engine-bound fingerprint and the hardware-bound hardwareFingerprint locally, and compareFingerprints is a synchronous function over two already-collected results. You will still store prior fingerprints somewhere to compare against historically, but that store is a key-value lookup, not a server-side matching engine.
Why does the hardware fingerprint stay the same across Chrome, Safari, and Firefox?
Because it is hashed only from signals that describe the physical device, not the browser engine. The GPU identity, CPU architecture, installed fonts, and screen geometry read the same regardless of which browser is asking. Engine-bound signals such as canvas and audio output, which differ between engines, are kept out of that hash and contribute to the per-browser fingerprint instead.
If it is just a hash compare, why is it hard to build?
The hashing is trivial; the partition is not. Some signals look hardware-bound but leak engine-specific details, and including even one in the hardware hash breaks cross-browser stability. The engineering is deciding which side of the line each signal sits on and keeping that invariant stable as browsers change, not the comparison itself.
What happens when a hardware signal genuinely changes, like a new monitor?
Hardware-bound signals can drift on legitimate events such as a display change, an OS upgrade, or a driver update. Strict equality on hardwareFingerprint handles the common cross-browser case, and compareFingerprints offers graded modes that apply per-signal fuzzy matching with a matchScore when you expect drift, so a returning user months later is not treated as a brand-new device.
Can an anti-detect browser defeat the cross-browser hash?
Anti-fingerprinting and anti-detect browsers inject noise that can perturb the signals the hash depends on, which is why the consistency score is reported alongside the identity. Brave-aware handling resolves known privacy-browser cases at a category level, and a high spoofLikelihood flags an environment that is actively forging its surface, so the identity and the trustworthiness of that identity stay separate.
Get started
Read how the split actually works
The hardware-versus-engine partition is the whole game. See the concept docs for the taxonomy and the comparison API, or npm install doorman-benny and read both hashes off a single call.
Last reviewed June 17, 2026

