Meet Janitor v0.2.0

A little history.
A familiar
browser.

Durable first-party identity for a browser that changes. Small histories, transparent matching, and optional AI risk signals.

TypeScript+MIT licensed+Your database
observation_historyContinuity, over time
visitorId: "vis_7a92…"Illustrative history
Fits where you build.
Cloudflare
Vercel
Node.js
Postgres
Built for ordinary change

Browsers evolve.
Identity should keep up.

A browser isn’t one immutable fingerprint. It’s a pattern of observations: a familiar device, an updated browser, a different window size.

Janitor keeps a stable ID alongside a small history. A first-party cookie handles the usual visit. When it’s gone, bounded similarity matching can recover the connection.

Read the matching algorithm
The identity lab

Change a signal.
Follow the identity.

Try the real matching engine with a synthetic browser. Everything runs here, in memory.

Simulated browser
Chrome on macOS1440 × 900 · 8 cores

Synthetic signals only.
No browser data is collected.

visitor.identify()Initializing
Visitor identityvis_…

Starting the local matching engine.

Identity confidence
0Restore threshold 0.901
// Run a scenario to inspect the response.

Every scenario starts from the same saved history. Only “Return with cookie” retains the simulated cookie. These scores illustrate the algorithm, not measured accuracy. How matching works ↗

A small API. A useful answer.

Identity and risk.
You own the policy.

One call returns a visitor ID, a confidence score, and two narrow risk signals. You decide when to show a CAPTCHA—or do nothing at all.

{}

Your infrastructureD1 or Postgres. Cloudflare, Vercel, or Node. No separate identity service.

ƒ

Optional intelligenceJev helps evaluate drift and risk. Swap in your own classifier whenever you need.

Graceful fallbackIf the evaluator is unavailable, deterministic matching continues and risk defaults to zero.

Explore the API ↗
client.ts
import { createVisitorClient } from "@janitor/browser";

const visitor = createVisitorClient({
  endpoint: "/api/visitor",
});

const identity = await visitor.identify();

// Your application decides what happens next.
if (identity.risk.automation > 0.85) {
  showCaptcha();
}
Understand it in one sitting

Less machinery.
More visibility.

No dashboards, accounts, queues, or background workers. Just a few understandable pieces.

Observe

Modest browser signals.
Aggregate event counts.

Compare

At most 10 candidates.
Transparent feature weights.

Evaluate

Up to 3 candidates.
Optional typed judgments.

Remember

A stable, opaque ID.
A bounded history.

Every weighting constant is in the source.Inspect the implementation ↗
Deliberately limited

Recognize a browser.
Respect its boundaries.

No raw IP addresses. No keystrokes, mouse coordinates, or form contents. No attempts to unmask signals a browser chooses to hide. History can be deleted, and retention is yours to configure.

See the full signal inventory ↗
Open source, from the first observation

Make yourself
familiar.

Start with the examples. Read the source.
Make Janitor fit your application.

Open the quickstart ↗View on GitHub ↗

Search documentation

Search the API, guides, and implementation notes.

Local search. No query leaves your browser.