A little history.
A familiar
browser.
Durable first-party identity for a browser that changes. Small histories, transparent matching, and optional AI risk signals.
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 algorithmChange a signal.
Follow the identity.
Try the real matching engine with a synthetic browser. Everything runs here, in memory.
Synthetic signals only.
No browser data is collected.
Starting the local matching engine.
// 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 ↗
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.
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();
}import { createCloudflareVisitor }
from "@janitor/adapters/cloudflare";
export default {
async fetch(request, env) {
const visitor = createCloudflareVisitor({
db: env.VISITORS,
ai: env.AI,
});
return visitor.handle(request);
},
};import { createVercelVisitor }
from "@janitor/adapters/vercel";
const visitor = createVercelVisitor({
db, // Your Postgres pool.
evaluator: { apiKey: process.env.JEV_API_KEY! },
});
export async function POST(request: Request) {
return visitor.handle(request);
}import { createNodeVisitor }
from "@janitor/adapters/node";
const visitor = createNodeVisitor({
db, // Any compatible Postgres pool.
evaluator: { apiKey: process.env.JEV_API_KEY! },
});
// Standard Web Request / Response.
const response = await visitor.handle(request);
// Maintenance runs on your schedule.
await visitor.cleanup();Less machinery.
More visibility.
No dashboards, accounts, queues, or background workers. Just a few understandable pieces.
Modest browser signals.
Aggregate event counts.
At most 10 candidates.
Transparent feature weights.
Up to 3 candidates.
Optional typed judgments.
A stable, opaque ID.
A bounded history.
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 ↗Make yourself
familiar.
Start with the examples. Read the source.
Make Janitor fit your application.