Skip to content

AI data loss prevention for Next.js

Users paste sensitive data into AI prompts – card numbers, phone numbers, home addresses, and whole résumés – often without realizing the risk. Once that data reaches your AI provider it can end up in logs, training pipelines, or model outputs, well outside your control.

Arcjet sensitive info detection scans prompt content inside your application, before it reaches the AI provider. Detection runs locally in your own environment, so the raw text never leaves your app: only the decision – whether sensitive data was found – is reported to Arcjet. When something is detected you choose what happens next: block the request, strip the data, or warn the user.

In this example we use the Vercel AI SDK to create a simple AI chat endpoint with Next.js, and Arcjet to prevent sensitive information from being sent to the AI model. The same principles can be applied to any AI application, including those built with other frameworks.

We assume you already have a Next.js app set up.

Install the dependencies:

Terminal window
# Export your Arcjet API key from https://console.arcjet.com
export ARCJET_KEY="ajkey_..."
npm install @arcjet/next ai @ai-sdk/openai

Create an AI chat endpoint:

/app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import arcjet, { sensitiveInfo } from "@arcjet/next";
import type { UIMessage } from "ai";
import { convertToModelMessages, isTextUIPart, streamText } from "ai";
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://console.arcjet.com
rules: [
sensitiveInfo({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
// Block PII types that should never appear in AI prompts.
// Remove types your app legitimately handles (for example, EMAIL for a support bot).
deny: ["CREDIT_CARD_NUMBER", "EMAIL"],
}),
],
});
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
// Check the most recent user message for sensitive information.
// Pass the full conversation if you want to scan all messages.
const lastMessage: string = (messages.at(-1)?.parts ?? [])
.filter(isTextUIPart)
.map((p) => p.text)
.join(" ");
const decision = await aj.protect(req, { sensitiveInfoValue: lastMessage });
if (decision.isDenied() && decision.reason.isSensitiveInfo()) {
console.warn("Request blocked due to sensitive information");
return new Response(
"Sensitive information detected – remove it from your prompt",
{ status: 400 },
);
}
// Arcjet approved – call your AI provider
const result = await streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}

And hook it up to a chat UI:

/app/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { messages, sendMessage } = useChat({
onError: async (e) => setErrorMessage(e.message),
});
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === "user" ? "User: " : "AI: "}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
))}
{errorMessage && (
<div className="text-red-500 text-sm mb-4">{errorMessage}</div>
)}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
setErrorMessage(null);
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}

Then run the server:

Terminal window
npm run dev

Requests appear in your Arcjet dashboard in real time.

Sensitive info detection runs through a detection backend – the engine that scans the text and identifies entities. There are two:

  • Built-in engine (default). A WebAssembly engine bundled with the SDK. It detects four structured types – card numbers, email addresses, phone numbers, and IP addresses – runs anywhere the SDK runs (including edge runtimes), and needs no extra dependencies.
  • Rampart backend (optional). An on-device named-entity-recognition (NER) model that adds the free-form PII people actually paste into prompts – names, street addresses, and government or financial identifiers. This is often the more valuable engine for AI data loss prevention, because that is exactly the data a structured-pattern matcher can’t catch.

Both engines run entirely on your own infrastructure. Nothing is sent to a third party for analysis, which is what makes this safe to put in front of an AI provider in the first place.

Use deny to list the entity types to block, or allow to block everything except the types you list (the two are mutually exclusive). Tune the list to your app – for a support bot that legitimately collects phone numbers, leave PHONE_NUMBER out of deny:

sensitiveInfo({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
deny: ["CREDIT_CARD_NUMBER", "EMAIL"],
});

The built-in engine detects CREDIT_CARD_NUMBER, PHONE_NUMBER, EMAIL, and IP_ADDRESS. See the entity detection table for every type each backend supports, and for defining your own custom detectors.

The built-in types cover structured data, but AI prompts are full of free-form PII – a pasted résumé, a shipping address, and someone’s full name. The optional Rampart backend runs an on-device NER model that detects these, plus government and financial identifiers (SSNs, tax IDs, passports, driver’s licenses, bank and routing numbers).

Install the package and pass rampart() as the rule’s backend:

Terminal window
npm install @arcjet/sensitive-info-rampart
import arcjet, { sensitiveInfo } from "@arcjet/next";
import { rampart } from "@arcjet/sensitive-info-rampart";
// The Rampart backend loads a native ONNX runtime, so this route must run on
// the Node.js runtime, not the Edge runtime.
export const runtime = "nodejs";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
sensitiveInfo({
mode: "LIVE",
// Names and addresses are common in pasted résumés and personal messages.
deny: [
"CREDIT_CARD_NUMBER",
"EMAIL",
"GIVEN_NAME",
"SURNAME",
"STREET_NAME",
],
backend: rampart(),
}),
],
});

The model is bundled with the package (~14.7 MB, quantized) so nothing is fetched at runtime, and inference is fast enough to run inline on each request (~6.6 ms median on Node.js). It recalls ~98% of private terms across seven Latin-script languages. Because it loads a native ONNX runtime it needs a server runtime – Node.js, Bun, or Deno, not edge – and on Next.js you must mark it as a server external package. For the bundler configuration, options, and the full accuracy and latency breakdown, see the Rampart reference.

Pass the text to scan as sensitiveInfoValue (JS) / sensitive_info_value (Python). For a chat endpoint this is usually the user’s most recent message. Pass the full conversation history instead if you want to scan every message, not just the latest one – useful when PII may have been introduced earlier in the exchange.

Set mode to "DRY_RUN" (JS) / Mode.DRY_RUN (Python) to log detections without blocking any requests. Run this in production for a while to audit what PII actually shows up in your prompts, then switch to "LIVE" once you’re confident in the entity list.

Sensitive info detection controls what data reaches your AI provider. Pair it with the other AI protection layers for full coverage: