AI data loss prevention for Next.js
People often paste personal data into AI prompts – card numbers, phone numbers, home addresses, and whole résumés. Once that data reaches your AI provider, the provider’s logging and retention apply to it, not yours.
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 doesn’t leave your app. The SDK reports the decision to Arcjet with the type and position of each finding, but not the matched text. When Arcjet finds something, you decide what happens next: block the request, remove the data, or ask the user to edit their prompt.
Get started
Section titled “Get started”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:
# Export your Arcjet API key from https://console.arcjet.comexport ARCJET_KEY="ajkey_..."
npm install @arcjet/next @arcjet/sensitive-info-rampart ai @ai-sdk/openaiCreate an AI chat endpoint:
import { openai } from "@ai-sdk/openai";import arcjet, { sensitiveInfo } from "@arcjet/next";import { rampart } from "@arcjet/sensitive-info-rampart";import type { UIMessage } from "ai";import { convertToModelMessages, isTextUIPart, streamText } from "ai";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your Arcjet 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"], backend: rampart(), }), ],});
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 }, ); }
// Block other denials too, such as one from a remote rule. if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
// Fail closed when Arcjet couldn't complete the check. if (decision.isErrored()) { return new Response("Service unavailable", { status: 503 }); }
// 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:
"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:
npm run devRequests appear in your Arcjet Console in real time.
How detection works
Section titled “How detection works”Sensitive info detection runs on-device with Rampart, a named-entity-recognition (NER) model paired with deterministic recognizers:
- Deterministic recognizers match structured types with validated patterns – card numbers, email addresses, IP addresses, URLs, and Social Security numbers.
- The NER model detects the free-form PII that people paste into prompts – names, street addresses, phone numbers, and government or financial identifiers. These don’t follow a fixed pattern, so a model finds them where pattern matching can’t.
The SDK rule runs detection on your own infrastructure, so the text isn’t sent to Arcjet or a third party for analysis. That makes it a good fit in front of an AI provider. The server-side sensitive information detector in an agent guard policy and coding agent hooks screen text on Arcjet’s side instead. Use the server-side detector for text longer than the SDK scans, and to detect protected health information (PHI) and secrets such as API keys, which on-device detection doesn’t cover.
Configure detection
Section titled “Configure detection”Choose which PII to block
Section titled “Choose which PII to block”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"], backend: rampart(),});For every entity type and where Arcjet detects it, see Entity types.
Detect names, addresses, and IDs
Section titled “Detect names, addresses, and IDs”AI prompts are full of free-form PII – a pasted résumé, a shipping address,
and someone’s full name. The
Rampart
NER model detects these, plus government and financial identifiers (SSNs, tax
IDs, passports, driver’s licenses, bank and routing numbers). Add them to
deny:
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 ships with the package (about 14.7 MB, quantized), so nothing is fetched at runtime, and inference is fast enough to run inline on each request (about 6.6 ms median on Node.js). It recalls about 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 mark it as a server external package. For the bundler configuration, options, and the full accuracy and latency breakdown, see the Rampart reference.
Choose what text to scan
Section titled “Choose what text to scan”Pass the text to scan as sensitiveInfoValue in JavaScript,
sensitive_info_value in Python, or WithSensitiveInfoValue in Go. For a chat
endpoint, this is usually the user’s most recent message. To scan every message,
pass the full conversation history instead – PII can appear earlier in the
exchange.
Test before you block
Section titled “Test before you block”Set mode to "DRY_RUN" in JavaScript, Mode.DRY_RUN in Python, or
ModeDryRun in Go to log detections without blocking requests. Run it in
production to see which PII appears in your prompts, then switch to live mode
when the entity list matches what you want to block.
Combine with other protections
Section titled “Combine with other protections”Sensitive info detection controls what data reaches your AI provider. Pair it with the other AI protection layers, each of which covers a different risk:
- Prompt injection detection blocks hostile instructions and jailbreak attempts.
- AI abuse protection and AI budget control block automated clients and enforce per-user token budgets.
- Agent guards apply PII detection directly inside agent tool handlers and pipelines that don’t route through HTTP. A local policy input keeps the raw string in the SDK while reporting policy evidence to Arcjet.
- Protected health information (PHI) detection finds medical record numbers, health plan member IDs, and vehicle and device identifiers with the server-side detector in an agent guard policy.