Skip to content

Sensitive information reference for Next.js

Arcjet Sensitive Information Detection protects against clients sending you sensitive information such as PII that you do not wish to handle. All detection runs locally in your own environment with the on-device Rampart NER model, which detects names, addresses, and government or financial identifiers as well as structured types such as email addresses and card numbers.

Sensitive information detection is configured by allowing or denying a subset of sensitive information. The allow and deny lists are mutually exclusive. With allow, Arcjet denies any detected sensitive information that the list does not name. With deny, Arcjet allows any detected sensitive information that the list does not name.

The arcjet client can be configured with one or more Sensitive information rules. Construct them with sensitiveInfo(...) in JavaScript / TypeScript, detect_sensitive_info(...) in Python, or arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{...}) in Go. The lists are allow / deny in JS and Python, and Allow / Deny in Go.

Set backend to the Rampart backend: rampart() in JS and Python, or the Backend 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 middleware

Bot protection rules can be configured in two ways:

  • Per API route: The rule is defined in the API route 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.
  • Middleware: The rule is defined in the middleware. 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.

Middleware

Avoid double protection with middleware

If you use Arcjet in middleware and individual routes, you need to be careful that Arcjet is not running multiple times per request. This can be avoided by excluding the API route from the middleware matcher.

For example, if you already have a sensitive info rule defined in the API route at /api/hello, you can exclude it from the middleware by specifying a matcher in /proxy.ts:

/middleware.ts
import arcjet, { createMiddleware, sensitiveInfo } from "@arcjet/next";
import { rampart } from "@arcjet/sensitive-info-rampart";
export const config = {
// The matcher prevents the middleware executing on static assets and the
// /api/hello API route because you already installed Arcjet directly
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/hello).*)"],
};
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
sensitiveInfo({
deny: ["EMAIL"],
mode: "LIVE",
backend: rampart(),
}),
],
});
// Pass any existing middleware with the optional existingMiddleware prop
export default createMiddleware(aj);

Pages and server actions

Arcjet can be used inside Next.js middleware, API routes, pages, server components, and server actions. Client components cannot be protected because they run on the client only.

See the Next.js SDK reference for examples of pages and page components and server actions.

Sensitive information detection runs on-device with the Rampart named-entity-recognition (NER) model, paired with deterministic recognizers for structured types. JavaScript and Python run a ~15 MB quantized ONNX model. Go embeds the same ~15 MB of weights in a nested module and infers in pure Go, with no cgo or ONNX runtime. The rule detects names, addresses, and government or financial identifiers, as well as email addresses, card numbers, IP addresses, and phone numbers. The model weights are bundled, so nothing is fetched at runtime and scanned text stays in your environment. For exactly what Rampart detects, see Entity detection.

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 is 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:

The full set of types Rampart supports is exported as rampartEntities (JS) / rampart_entities (Python) / rampart.Entities() (Go). Use it to deny every type Rampart can detect:

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

Rampart is a compact model – a ~14.7 MB artifact (~18.5M parameters, quantized to 4-bit weights) with a 512-token context window – so it is fast enough to run inline on each request. On the Node.js CPU runtime, median (p50) inference is ~6.6 ms, dropping to ~3.9 ms p50 on WebGPU (set device: "webgpu" in the JS SDK). The Python backend runs on ONNX Runtime and defaults to CPU execution; select a different accelerator with the providers option (described later on this page). The Go backend is a nested module that embeds the same ~15 MB weights and runs inference in pure Go (no cgo). Create it once with rampart.New and reuse it. The default MaxInputChars is 4096.

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

Accuracy varies by input, so it’s worth knowing the trade-offs:

  • 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.
  • Inference runs synchronously in the request path, so this latency is added to each request 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,
// 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 [];
},
],
});

In JavaScript and Python, the model loads once on first use and is reused for every subsequent request. In Go, rampart.New loads and dequantizes the weights – create the backend once at startup and reuse it. It is 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 bundled model weights from disk at runtime. As a result:

  • 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. Weights are compiled into the nested module, so there is no ONNX runtime, native addon, or extra file for a bundler to miss. Applications that import the module include about 15 MB of weights in the binary.

In Python, the backend depends on ONNX Runtime (onnxruntime), which is installed for you by the arcjet[sensitive-info-rampart] extra. Model inference is synchronous and blocking, so its latency is added directly to each request the rule scans – the same is true whether you use the arcjet() or 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. Also make sure any route handler that uses the backend runs on the Node.js runtime (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";

Arcjet provides a single protect function that is used to execute your protection rules.

This function returns a Promise that resolves to an ArcjetDecision object. This contains the following properties:

  • id (string) – The unique ID for the request. This can be used to look up the request in the Arcjet dashboard. It is prefixed with req_ for decisions involving the Arcjet cloud API. For decisions taken locally, the prefix is lreq_.
  • conclusion (ArcjetConclusion) – The final conclusion based on evaluating each of the configured rules. If you wish to accept Arcjet’s recommended action based on the configured rules then you can use this property.
  • reason (ArcjetReason) – An object containing more detailed information about the conclusion.
  • results (ArcjetRuleResult[]) – An array of ArcjetRuleResult objects containing the results of each rule that was executed.
  • ip (ArcjetIpDetails) – An object containing Arcjet’s analysis of the client IP address. For more information, see the SDK reference.

To check whether a sensitive info rule returned a deny conclusion, 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).

You can iterate through the results and check whether a sensitive info rule was applied:

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

This example logs the full result as well as the sensitive info rule:

Rampart detects the following entity types. 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. You can also detect more patterns with custom entity detection.

Entity typeDetected by
EMAILModel and recognizer
PHONE_NUMBERModel
IP_ADDRESSRecognizer
CREDIT_CARD_NUMBERRecognizer
URLModel and recognizer
SSNRecognizer
GIVEN_NAMEModel
SURNAMEModel
TAX_IDModel
BANK_ACCOUNTModel
ROUTING_NUMBERModel
GOVERNMENT_IDModel
PASSPORTModel
DRIVERS_LICENSEModel
BUILDING_NUMBERModel
STREET_NAMEModel
SECONDARY_ADDRESSModel
CITYModel
STATEModel
ZIP_CODEModel
CustomYour own recognizers

Model means the on-device Rampart NER model detects the type. Recognizer means a deterministic, validated pattern detects it – for example, card numbers must pass the Luhn checksum. Where the model and a recognizer overlap on the same text, the recognizer wins.

Phone numbers are left to the model because their digit shape overlaps with financial and government identifiers, so the default recognizers don’t include a phone number pattern.

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.

Card numbers of 13 to 19 digits are detected when the final digit is a valid Luhn check digit. The values 4242424242424242, 4000 0566 5566 5556, and 3782-8224-6310005 match but 4242424242424241 does not. Spaces and dashes are ignored because users often group digits.

The values alice@example.com and bob.smith@subdomain.example.com match but alice.example.com does not.

IPv4 and IPv6 addresses are detected. The values 127.0.0.1 and ::1 match.

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

URLs that start with http://, https://, or www. are detected. 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.

Arcjet is designed to fail open so that a service issue or misconfiguration does not block all requests. The SDK also times out and fails open after 2000 ms by default. However, in most cases, the response time is less than 20 ms to 30 ms.

If there is an error condition when processing the rule, Arcjet returns an ERROR result for that rule and you can check the message property on the rule’s error result for more information.

If all other rules that were run returned an ALLOW result, then the final Arcjet conclusion is ERROR.

Arcjet runs the same in any environment, including locally and in CI. You can use the mode set to DRY_RUN to log the results of rule execution without blocking any requests.

We have an example test framework you can use to automatically test your rules. Arcjet can also be triggered based using a sample of your traffic.

For details, see the Testing section of the docs.