Skip to content

Sensitive information reference for SvelteKit

Arcjet Sensitive Information Detection stops clients from sending you data that you don’t want to handle, such as personally identifiable information (PII). The SDK rule runs detection in your own environment with the on-device Rampart named-entity recognition (NER) model. It detects names, addresses, and government or financial identifiers, as well as structured types such as email addresses and card numbers. The text it scans never leaves your environment.

You configure sensitive information detection with a list of entity types to allow or to deny. The two lists are mutually exclusive:

  • With allow, Arcjet denies any detected type that the list doesn’t name.
  • With deny, Arcjet denies only the types that the list names.

Add one or more sensitive information rules to the arcjet client. Create them with sensitiveInfo(...) in JavaScript / TypeScript, detect_sensitive_info(...) in Python, or arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{...}) in Go. The lists are allow and deny in JS and Python, and Allow and Deny in Go.

Set backend to the Rampart backend: rampart() in JS and Python, or the Backend that you create with rampart.New in Go.

type SensitiveInfoOptionsAllow = {
mode?: "LIVE" | "DRY_RUN";
allow?: Array<ArcjetSensitiveInfoType>;
// The detection backend. Pass `rampart()` from
// `@arcjet/sensitive-info-rampart` (see below).
backend?: SensitiveInfoBackend;
};
type SensitiveInfoOptionsDeny = {
mode?: "LIVE" | "DRY_RUN";
deny?: Array<ArcjetSensitiveInfoType>;
// The detection backend. Pass `rampart()` from
// `@arcjet/sensitive-info-rampart` (see below).
backend?: SensitiveInfoBackend;
};
type ArcjetSensitiveInfoType =
| "EMAIL"
| "PHONE_NUMBER"
| "IP_ADDRESS"
| "CREDIT_CARD_NUMBER"
| "GIVEN_NAME"
| "SURNAME"
| "SSN"
| "URL"
| "TAX_ID"
| "BANK_ACCOUNT"
| "ROUTING_NUMBER"
| "GOVERNMENT_ID"
| "PASSPORT"
| "DRIVERS_LICENSE"
| "BUILDING_NUMBER"
| "STREET_NAME"
| "SECONDARY_ADDRESS"
| "CITY"
| "STATE"
| "ZIP_CODE";

Per route versus hooks

Bot protection rules can be configured in two ways:

  • Per route: The rule is defined in the route handler itself. This allows you to configure the rule alongside the code it is protecting which is useful if you want to use the decision to add context to your own code. However, it means rules are not located in a single place.
  • Hooks: The rule is defined as a hook. This lets you configure rules in a single place or apply them globally to all routes, but it means the rules are not located alongside the code they are protecting.

Per route

This configures bot protection on a single route.

Hooks

This runs on every request to your SvelteKit app – see the SvelteKit Hooks docs for details.

Avoid double protection with hooks

If you use Arcjet in hooks and individual routes, you need to be careful that Arcjet is not running multiple times per request. This can be avoided by excluding the individual routes before running Arcjet in the hook.

For example, if you already have a sensitive info rule defined in the API route at /api/arcjet, you can exclude it from the hook like this:

/src/hooks.server.ts
import { env } from "$env/dynamic/private";
import arcjet, { detectBot } from "@arcjet/sveltekit";
import { error, type RequestEvent } from "@sveltejs/kit";
const aj = arcjet({
key: env.ARCJET_KEY!,
rules: [
detectBot({
mode: "LIVE",
allow: [], // "allow none" will block all detected bots
}),
],
});
export async function handle({
event,
resolve,
}: {
event: RequestEvent;
resolve: (event: RequestEvent) => Response | Promise<Response>;
}): Promise<Response> {
// Ignore routes that extend the Arcjet rules
// - they will call `.protect` themselves
const filteredRoutes = ["/api/arcjet"];
if (filteredRoutes.includes(event.url.pathname)) {
// return - route will handle protecttion
return resolve(event);
}
const decision = await aj.protect(event);
if (decision.isDenied()) {
return error(403, "Forbidden");
}
return resolve(event);
}

Sensitive information detection runs on-device. It pairs the Rampart NER model, which understands context, with deterministic recognizers for structured types. The rule detects names, addresses, and government or financial identifiers, as well as email addresses, card numbers, IP addresses, and phone numbers. For the full list, see Entity types.

The model weights ship with the package, so nothing is fetched at runtime and the text you scan stays in your environment. JavaScript and Python run a ~15 MB quantized ONNX model. Go embeds the same ~15 MB of weights in a nested module and runs inference in pure Go, with no cgo or ONNX runtime.

Install the Rampart package alongside your Arcjet SDK. In Python, it ships as the sensitive-info-rampart extra of the arcjet package. In Go, it’s a nested module that bundles the model weights:

Terminal window
npm install @arcjet/sensitive-info-rampart

Then import rampart and pass it as the rule’s backend. Create the Go backend once at startup with rampart.New and reuse it. List the entity types to detect in allow or deny:

To deny every type that Rampart detects, use the full set: rampartEntities in JS, rampart_entities in Python, or rampart.Entities() in Go:

import { rampart, rampartEntities } from "@arcjet/sensitive-info-rampart";
sensitiveInfo({ mode: "LIVE", deny: rampartEntities, backend: rampart() });

Rampart is compact enough to run inline on each request: a ~14.7 MB artifact with ~18.5M parameters, quantized to 4-bit weights, and a 512-token context window. Longer text is scanned in overlapping windows. On the Node.js CPU runtime, median (p50) inference is ~6.6 ms, and ~3.9 ms on WebGPU (set device: "webgpu" in the JS SDK). The Python backend runs on ONNX Runtime and uses the CPU by default. To use a different accelerator, set the providers option. In Go, create the backend once with rampart.New and reuse it.

On the model author’s held-out benchmark, Rampart recalls ~98% of private terms across the seven Latin-script languages it supports: English, Spanish, French, German, Italian, Portuguese, and Dutch.

Accuracy depends on the input. Keep the following trade-offs in mind:

  • Non-Latin scripts (for example Chinese, Japanese, Korean, Arabic, Hindi, or Cyrillic) have much lower recall – the model is trained on Latin-script text.
  • Government or financial identifiers without a checksum (passports, licenses, case numbers) are recognized less reliably than the structured types the deterministic recognizers validate, such as emails and card numbers.
  • Latency – inference runs synchronously in the request path, so it adds to the response time of each request that the rule scans.

These figures come from the Rampart model card, which has the full per-language breakdown, calibration data, and limitations.

The rampart() factory (JS and Python) or rampart.New (Go) accepts these options:

import { defaultRecognizers, rampart } from "@arcjet/sensitive-info-rampart";
rampart({
// Execution device (default: "cpu"). Set to "webgpu" to use a GPU when the
// runtime supports it.
device: "webgpu",
// Minimum confidence score for a model token to count (default: 0.5).
threshold: 0.6,
// Largest input to scan, in characters (default: 100_000). The rule denies
// longer input without scanning it. Lower it to bound latency, or raise it
// to accept longer text.
maxInputChars: 100_000,
// Deterministic recognizers to run alongside the model (default:
// `defaultRecognizers`). This is the extension point for custom detection –
// add a recognizer that returns matched spans. Pass `[]` to rely on the
// model alone.
recognizers: [
...defaultRecognizers,
(value) => {
// Return an array of `{ start, end, type }` spans.
return [];
},
],
});

Rampart scans text on the request path, so longer text takes longer to scan. To keep that cost predictable, each SDK sets a maximum length, which you can change:

SDKDefault limitOption
JavaScript100,000 charactersmaxInputChars
Python100,000 charactersmax_input_chars
Go4,096 charactersMaxInputChars

Go’s default is lower because the Go backend runs the model in pure Go, with no native runtime to install, and each scan takes several times longer than with the ONNX Runtime that JavaScript and Python use.

A live rule denies text longer than the limit without scanning any of it, so nothing past the limit goes through unchecked. A rule in dry run mode doesn’t block: with protect() the SDK logs a warning, and with Guards Arcjet records the denial. The limit applies to protect(), to the local sensitive information rule in Arcjet Guards, and to a LOCAL input in an agent guard policy. JavaScript counts UTF-16 code units, and Python and Go count Unicode code points.

With protect(), the denial has an error reason instead of a sensitive information reason, because Arcjet didn’t detect anything. The examples on this page block it with their catch-all denial. To return a clearer response, check for the error reason: decision.reason.isError() in JavaScript, decision.reason_v2.type == "ERROR" in Python, or decision.Reason.IsError() in Go:

if (decision.isDenied() && decision.reason.isError()) {
// The text is longer than the backend's scan limit.
return Response.json({ error: "Message too long" }, { status: 413 });
}

With Guards, the rule result is a denial with no detected entity types.

Choose a limit that fits your latency budget. Each window of about 480 characters is one inference pass, so a higher limit raises the worst-case scan time for a request. The limit must be a positive whole number, and there’s no setting that turns it off. Also cap the request body size in your server or framework, so your application doesn’t read an oversized body into memory.

On-device detection suits typical request fields. For longer text, you have the following options:

  • Raise the limit when the extra latency fits your budget.
  • Use a server input. Send the text as a SERVER input to the server-side sensitive information detector in an agent guard policy, which screens values up to about 100 KB on Arcjet’s servers. Arcjet allows up to 3 seconds to screen a value over 2 KB, and a live rule fails closed if the scan doesn’t finish. Set the client timeout above 3 seconds (the default is 2), so the client doesn’t stop waiting first, and deny the request when hasFailedOpen() is true.
  • Split the text into parts, for text longer than about 100 KB, and screen each part in its own guard() call. Each call screens up to two SERVER inputs longer than 2 KB.

To detect protected health information (PHI), always use the server-side detector. On-device detection doesn’t detect the PHI types. For more information, see Detect protected health information (PHI).

In JavaScript and Python, the model loads on first use and is reused for every later request. In Go, rampart.New loads and dequantizes the weights, so create the backend once at startup and reuse it. It’s safe for concurrent use.

To detect your own entity types, add a recognizer. See Custom entity detection.

In JavaScript and Python, the Rampart backend loads a native ONNX runtime and reads its model weights from disk at runtime. This affects where it runs and how you build your application:

  • It requires a server runtime with filesystem and native-addon access. In JS that means Node.js, Bun, or Deno – it does not run on edge runtimes. In Python it runs anywhere the SDK does (Python 3.10+).
  • A server build must not bundle it, or the runtime fails to locate the model weights.

In Go, inference is pure Go. The weights are compiled into the nested module, so there’s no ONNX runtime, native addon, or extra file for a bundler to miss. An application that imports the module adds about 15 MB of weights to its binary.

In Python, the backend depends on ONNX Runtime (onnxruntime), which the arcjet[sensitive-info-rampart] extra installs. Inference is synchronous, so it adds to the response time of each request that the rule scans, with either the arcjet() or the arcjet_sync() client.

For Next.js, mark the package and its native dependencies as server external packages, so that Next.js loads them from node_modules at runtime instead of bundling them. Run each route handler that uses the backend on the Node.js runtime, which is the default, rather than the Edge runtime:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: [
"@arcjet/sensitive-info-rampart",
"@huggingface/transformers",
"onnxruntime-node",
],
};
module.exports = nextConfig;
app/api/arcjet/route.ts
export const runtime = "nodejs";

To evaluate your rules, call the protect function.

The function returns a Promise that resolves to an ArcjetDecision object with the following properties:

  • id (string) – The unique ID for the request. Use it to look up the request in the Arcjet Console. Decisions that involve the Arcjet cloud API have the prefix req_, and decisions made locally have the prefix lreq_.
  • conclusion (ArcjetConclusion) – The overall conclusion from evaluating every configured rule. To follow Arcjet’s recommended action, use this property.
  • reason (ArcjetReason) – More detail about the conclusion.
  • results (ArcjetRuleResult[]) – The result of each rule that ran.
  • ip (ArcjetIpDetails) – Arcjet’s analysis of the client IP address. For more information, see the SDK reference.

To check whether a sensitive information rule denied the request, use decision.isDenied() and decision.reason.isSensitiveInfo() (JS) / decision.is_denied() and decision.reason_v2.type == "SENSITIVE_INFO" (Python) / decision.IsDenied() and decision.Reason.IsSensitiveInfo() (Go).

To see what each rule found, iterate through the results:

for (const result of decision.results) {
console.log("Rule Result", result);
}

The following example logs each result and the sensitive information rule:

For every entity type that Arcjet detects, grouped by category with its taxonomy version and where Arcjet detects it, see Sensitive information entity types. The SDK detects the version 1 types with Rampart. List any of them in allow or deny. Each one is an ArcjetSensitiveInfoType in JS, a SensitiveInfoEntityType in Python, and an arcjet.EntityType in Go.

To list every type at once, use rampartEntities (JS), rampart_entities (Python), or rampart.Entities() (Go).

The default recognizers match these structured types with deterministic, validated patterns.

Arcjet detects card numbers of 13 to 19 digits when the final digit is a valid Luhn check digit. The values 4242424242424242, 4000 0566 5566 5556, and 3782-8224-6310005 match, but 4242424242424241 doesn’t. Spaces and dashes are ignored, because people often group the digits.

The values alice@example.com and bob.smith@subdomain.example.com match, but alice.example.com doesn’t.

Arcjet detects IPv4 and IPv6 addresses. The values 127.0.0.1 and ::1 match.

Arcjet detects US Social Security numbers in the dashed form, such as 123-45-6789.

Arcjet detects URLs that start with http://, https://, or www.. The model also detects URLs from context.

The Rampart model detects phone numbers from context. The default recognizers don’t include a phone number pattern, because a run of digits can also be a bank account, routing number, or government identifier, and a pattern can’t tell which one it is.

To detect patterns that the default recognizers don’t cover, add your own recognizer to the Rampart recognizers option. A recognizer receives the full text and returns the spans it matched, each with a start offset, an end offset, and an entity type. Recognizer results take precedence over the model where they overlap.

Keep the default recognizers in the list unless you want to replace them. The following example adds a recognizer that reports IBANs as BANK_ACCOUNT:

Offsets are UTF-16 code unit indexes in JS, code point indexes in Python, and byte offsets in Go – the indexes each language’s regular expression matches return.

By default, Arcjet fails open, so a service issue or misconfiguration doesn’t block your users. The SDK times out and fails open after 2,000 ms. Most responses take 20 ms to 30 ms.

If a rule hits an error, Arcjet returns an ERROR result for that rule. The message property on the result explains what went wrong. If every other rule that ran returned ALLOW, the overall conclusion is ERROR.

Sensitive information detection protects data, so deny the request when the decision is errored, unless your application can accept data that Arcjet didn’t screen. The quick start examples fail closed this way. The following example logs the error and shows where to fail closed:

Arcjet runs the same way in every environment, including locally and in CI. To log the results of your rules without blocking any requests, set mode to DRY_RUN.

You can test your rules automatically with a test framework, or with a sample of your traffic. For more information, see Testing.