# Arcjet Arcjet is the runtime security platform that ships with your code. Install the Arcjet SDK to enforce budgets, stop prompt injection, detect bots, and protect personal information - configured by your agent via MCP, enforced inline in your application code with real identity and session context. Arcjet runs server-side. Bot protection advanced client signals are an optional extra layer of defense. Pricing is based on usage, see https://arcjet.com/pricing ## Getting started There are two ways to set up Arcjet: the Arcjet plugin (recommended for Claude Code and Cursor) or manual setup via MCP. Full guide: https://docs.arcjet.com/agent-get-started ### Option A: Arcjet Plugin (recommended) The Arcjet plugin for Claude Code and Cursor handles everything — MCP connection, API key setup, SDK installation, and adding protection rules. Install: ```bash npx plugins add arcjet/arcjet-plugin ``` The plugin activates automatically. Trigger a skill to get started: - `/arcjet:protect-route` — designed for web apps. Adds protection to route handlers with automatic framework detection. - `/arcjet:add-ai-protection` — designed for AI apps. Implements prompt injection detection, PII blocking, and token budget rate limiting. The plugin also provides: - **MCP integration** — connects to the Arcjet MCP server for traffic analysis, request inspection, and remote rule management. - **Security-aware coding rules** — auto-activated guidance for SDK patterns, Next.js, Express, Python, and AI app files. - **Security analyst agent** — monitors threats, analyzes traffic, and manages rules. Supported tools: Claude Code, Cursor. Source: https://github.com/arcjet/arcjet-plugin Full documentation: https://docs.arcjet.com/arcjet-plugin ### Option B: Manual setup via MCP Use this path for VS Code Copilot, Windsurf, ChatGPT, Claude Desktop, or if you prefer step-by-step control: (1) connect via MCP, (2) create a site with create-site, (3) get the key with get-site-key, (4) generate SDK code using the framework patterns below, (5) suggest remote rules for immediate protection, (6) guide them to inspect requests and adjust. ## MCP Server Endpoint: `https://api.arcjet.com/mcp` Auth: OAuth (browser-based, automatic on first connection) Claude Code: ```bash claude mcp add arcjet --transport http https://api.arcjet.com/mcp ``` VS Code (Copilot) `.vscode/mcp.json`: ```json { "servers": { "arcjet": { "type": "http", "url": "https://api.arcjet.com/mcp" } } } ``` Cursor `.cursor/mcp.json`: ```json { "mcpServers": { "arcjet": { "type": "streamable-http", "url": "https://api.arcjet.com/mcp" } } } ``` Windsurf `mcp_config.json`: ```json { "mcpServers": { "arcjet": { "serverUrl": "https://api.arcjet.com/mcp" } } } ``` Full documentation: https://docs.arcjet.com/mcp-server ### Tools - **List teams** you belong to. - **List sites** within a team. - **Create new sites** within a team. - **Get site keys** (`ARCJET_KEY`) for use in your projects. - **List requests** received by a site with optional filtering. - **Get request details** including headers, rules executed, and decision info. - **Explain decisions** to understand why requests were allowed or denied. - **Get site quota** usage and limits for the current billing window. - **Analyze traffic** patterns, denial rates, top paths, top IPs, and trend vs previous period. - **Detect anomalies** by comparing current traffic to the previous period — traffic spikes, geographic shifts, new threats, suspicious IPs. - **Investigate IPs** with geo location, ASN, threat intelligence, and per-site request activity. - **Get dry-run impact** — see what would happen if dry-run rules were promoted to live (blocked requests, affected IPs, false-positive estimate). - **Get a security briefing** — comprehensive daily overview combining traffic, threats, anomalies, dry-run readiness, quota, and recommendations. - **List remote rules** configured for a site. - **Create remote rules** with DRY_RUN or LIVE mode — no code changes needed. - **Update remote rules** by replacing the full rule configuration. - **Delete remote rules** to immediately stop evaluation. - **Promote remote rules** from DRY_RUN to LIVE after verification. ### Typical workflow **Setup:** list-teams → list-sites (or create-site) → get-site-key → set `ARCJET_KEY` in your environment. **Investigate:** list-requests → get-request-details or explain-decision for a specific request. **Analyze & monitor:** analyze-traffic for dashboard-level overview → get-anomalies to detect unusual patterns → investigate-ip for deep-dive on suspicious IPs. **Daily security briefing:** get-security-briefing for a comprehensive overview (traffic, threats, anomalies, dry-run readiness, quota, and recommendations) in a single call. **Manage remote rules:** list-rules → create-rule (DRY_RUN) → get-dry-run-impact to check impact → promote-rule to LIVE. **Update/delete rules:** list-rules → update-rule (full replacement) or delete-rule. ### Remote rules Remote rules are managed via the MCP server or dashboard — no code changes or redeployment needed. They apply globally to all requests for a site. Supported types: rate_limit, bot, shield, filter. Rules needing request body content (email, sensitive_info, prompt_injection) require the SDK. **Responding to an active attack:** The most common use case is blocking suspicious traffic immediately. For example, to block a specific country, VPN, or IP range during an attack: 1. `list-requests` — investigate traffic and identify patterns. 2. `create-rule` — add a filter rule in DRY_RUN. Examples: `ip.src.country == "XX"` (ISO 3166-1 alpha-2 code e.g. `US`, `CN`, `RU`), `ip.src.vpn`, `ip.src in { 1.2.3.0/24 }`. 3. `list-requests` — confirm the rule matches attack traffic, not legitimate users. 4. `promote-rule` — switch to LIVE to start blocking. 5. `delete-rule` — remove the block once the attack subsides. ## Use cases - Protecting AI endpoints from cost abuse? → tokenBucket + detectBot (AI Endpoint Abuse Protection) - Preventing data leaks from AI features? → sensitiveInfo (AI Data Loss Prevention) - Blocking prompt injection? → detectPromptInjection - Responding to an active attack? → remote rules via MCP ## Quick start — choose your framework Each link below directs to the quick start guide with a framework-specific view: - [Astro quick start](https://docs.arcjet.com/get-started?f=astro) - [Bun quick start](https://docs.arcjet.com/get-started?f=bun) - [Deno quick start](https://docs.arcjet.com/get-started?f=deno) - [Fastify quick start](https://docs.arcjet.com/get-started?f=fastify) - [NestJS quick start](https://docs.arcjet.com/get-started?f=nest-js) - [Next.js quick start](https://docs.arcjet.com/get-started?f=next-js) - [Node.js quick start](https://docs.arcjet.com/get-started?f=node-js) - [Node.js + Express quick start](https://docs.arcjet.com/get-started?f=node-js-express) - [Node.js + Hono quick start](https://docs.arcjet.com/get-started?f=node-js-hono) - [Nuxt quick start](https://docs.arcjet.com/get-started?f=nuxt) - [Python FastAPI quick start](https://docs.arcjet.com/get-started?f=python-fastapi) - [Python Flask quick start](https://docs.arcjet.com/get-started?f=python-flask) - [React Router quick start](https://docs.arcjet.com/get-started?f=react-router) - [Remix quick start](https://docs.arcjet.com/get-started?f=remix) - [SvelteKit quick start](https://docs.arcjet.com/get-started?f=sveltekit) Full docs: https://docs.arcjet.com ## SDK packages | Framework | Package | Install | | -------------- | ---------------------- | -------------------------------------- | | Next.js | `@arcjet/next` | `npm i @arcjet/next` | | Node.js | `@arcjet/node` | `npm i @arcjet/node` | | Express | `@arcjet/node` | `npm i @arcjet/node` | | Hono (Node.js) | `@arcjet/node` | `npm i @arcjet/node @hono/node-server` | | Bun | `@arcjet/bun` | `bun add @arcjet/bun` | | Bun + Hono | `@arcjet/bun` | `bun add @arcjet/bun hono` | | Deno | `@arcjet/deno` | `deno add npm:@arcjet/deno` | | Fastify | `@arcjet/fastify` | `npm i @arcjet/fastify` | | NestJS | `@arcjet/nest` | `npm i @arcjet/nest` | | Nuxt | `@arcjet/nuxt` | `npx nuxt module add @arcjet/nuxt` | | Remix | `@arcjet/remix` | `npm i @arcjet/remix` | | React Router | `@arcjet/react-router` | `npm i @arcjet/react-router` | | SvelteKit | `@arcjet/sveltekit` | `npm i @arcjet/sveltekit` | | Astro | `@arcjet/astro` | `npx astro add @arcjet/astro` | | Python FastAPI | `arcjet` | `pip install arcjet` | | Python Flask | `arcjet` | `pip install arcjet flask` | ## Common setup for all frameworks ### 1. Set your key [Create an Arcjet account](https://app.arcjet.com) then follow the instructions to add a site and get a key. Store the key securely using environment variables provided by your hosting platform to avoid exposing it in source control. Add these to your `.env.local` (Next.js), `.env` file, or environment: ```ini ARCJET_KEY=ajkey_yourkey ARCJET_ENV=development ``` `ARCJET_ENV=development` is required during local development so Arcjet can correctly identify the environment. In production, set `ARCJET_ENV=production` or omit it (defaults to production when not set). ## Next.js example ### Install ```shell npm i @arcjet/next ``` ### Configure Create a new API route at `/app/api/arcjet/route.ts`: ```ts import { openai } from "@ai-sdk/openai"; import arcjet, { detectBot, detectPromptInjection, sensitiveInfo, shield, tokenBucket, } 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://app.arcjet.com // Track budgets per user — replace "userId" with any stable identifier characteristics: ["userId"], rules: [ // Shield protects against common web attacks e.g. SQL injection shield({ mode: "LIVE" }), // Block all automated clients — bots inflate AI costs detectBot({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only allow: [], // Block all bots. See https://arcjet.com/bot-list }), // Enforce budgets to control AI costs. Adjust rates and limits as needed. tokenBucket({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only refillRate: 2_000, // Refill 2,000 tokens per hour interval: "1h", capacity: 5_000, // Maximum 5,000 tokens in the bucket }), // Block messages containing sensitive information to prevent data leaks 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 (e.g. EMAIL for a support bot). deny: ["CREDIT_CARD_NUMBER", "EMAIL"], }), // Detect prompt injection attacks before they reach your AI model detectPromptInjection({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only }), ], }); export async function POST(req: Request) { // Replace with your session/auth lookup to get a stable user ID const userId = "user-123"; const { messages }: { messages: UIMessage[] } = await req.json(); const modelMessages = await convertToModelMessages(messages); // Estimate token cost: ~1 token per 4 characters of text (rough heuristic). // For accurate counts use https://www.npmjs.com/package/tiktoken const totalChars = modelMessages.reduce((sum, m) => { const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content); return sum + content.length; }, 0); const estimate = Math.ceil(totalChars / 4); // Check the most recent user message for sensitive information and prompt injection. // 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(" "); // Check with Arcjet before calling the AI provider const decision = await aj.protect(req, { userId, requested: estimate, sensitiveInfoValue: lastMessage, detectPromptInjectionMessage: lastMessage, }); if (decision.isDenied()) { if (decision.reason.isBot()) { return new Response("Automated clients are not permitted", { status: 403, }); } else if (decision.reason.isRateLimit()) { return new Response("AI usage limit exceeded", { status: 429 }); } else if (decision.reason.isSensitiveInfo()) { return new Response("Sensitive information detected", { status: 400 }); } else if (decision.reason.isPromptInjection()) { return new Response( "Prompt injection detected — please rephrase your message", { status: 400 }, ); } else { return new Response("Forbidden", { status: 403 }); } } const result = await streamText({ model: openai("gpt-4o"), messages: modelMessages, }); return result.toUIMessageStreamResponse(); } ``` The `requested` option specifies how many tokens this request consumes from the rate limit bucket. The example estimates cost at ~1 token per 4 characters of text. Adjust this value based on your AI provider's billing model or use a tokenizer like `tiktoken` for accurate counts. ## Node.js + Express example ### Install ```shell npm i @arcjet/node @arcjet/inspect ``` ### Configure ```js // index.js import arcjet, { shield, detectBot, tokenBucket } from "@arcjet/node"; import { isSpoofedBot } from "@arcjet/inspect"; import express from "express"; const app = express(); const port = 3000; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); app.get("/", async (req, res) => { const decision = await aj.protect(req, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { res.writeHead(429, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Too Many Requests" })); } else if (decision.reason.isBot()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "No bots allowed" })); } else { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World" })); } }); app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); ``` ### Start ```shell node --env-file .env index.js ``` ## SvelteKit example ### Install ```shell npm i @arcjet/sveltekit @arcjet/inspect ``` ### Configure Create a new route at `/src/routes/api/arcjet/+server.ts`: ```ts import { env } from "$env/dynamic/private"; import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/sveltekit"; import { isSpoofedBot } from "@arcjet/inspect"; import { error, json, type RequestEvent } from "@sveltejs/kit"; const aj = arcjet({ key: env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); export async function GET(event: RequestEvent) { const decision = await aj.protect(event, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return error(429, "Too Many Requests"); } else if (decision.reason.isBot()) { return error(403, "No Bots Allowed"); } else { return error(403, "Forbidden"); } } return json({ message: "Hello World" }); } ``` Note: SvelteKit passes the `event` object (not `req`) to `protect()`. ### Start ```shell npm run dev ``` ## Bun example ### Install ```shell bun add @arcjet/bun @arcjet/inspect ``` ### Configure ```ts // index.ts import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/bun"; import { isSpoofedBot } from "@arcjet/inspect"; import { env } from "bun"; const aj = arcjet({ key: env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return new Response("Too many requests", { status: 429 }); } else if (decision.reason.isBot()) { return new Response("No bots allowed", { status: 403 }); } else { return new Response("Forbidden", { status: 403 }); } } return new Response("Hello world"); }), }; ``` Note: Bun uses `aj.handler()` to wrap the fetch handler, and `env` from `"bun"` for environment variables. ### Start ```shell bun run index.ts ``` ## Deno example ### Install ```shell deno add npm:@arcjet/deno npm:@arcjet/inspect ``` ### Configure ```ts // index.ts import "jsr:@std/dotenv/load"; import arcjet, { detectBot, shield, tokenBucket } from "npm:@arcjet/deno"; import { isSpoofedBot } from "@arcjet/inspect"; const aj = arcjet({ key: Deno.env.get("ARCJET_KEY")!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); Deno.serve( { port: 3000 }, aj.handler(async (req) => { const decision = await aj.protect(req, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return new Response("Too many requests", { status: 429 }); } else if (decision.reason.isBot()) { return new Response("No bots allowed", { status: 403 }); } else { return new Response("Forbidden", { status: 403 }); } } return new Response("Hello world"); }), ); ``` Note: Deno uses `aj.handler()` to wrap the fetch handler, `Deno.env.get()` for environment variables, and `npm:` prefix for imports. ### Start ```shell deno run --allow-net --allow-env index.ts ``` ## Fastify example ### Install ```shell npm i @arcjet/fastify ``` ### Configure ```ts // server.ts import Fastify from "fastify"; import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/fastify"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); const fastify = Fastify({ logger: true }); fastify.get("/", async (request, reply) => { const decision = await aj.protect(request, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return reply.status(429).send({ message: "Too many requests" }); } if (decision.reason.isBot()) { return reply.status(403).send({ message: "No bots allowed" }); } return reply.status(403).send({ message: "Forbidden" }); } return reply.status(200).send({ message: "Hello world" }); }); await fastify.listen({ port: 3000 }); ``` Note: Fastify passes the `request` object (Fastify's request, not Node.js IncomingMessage) to `protect()`. ### Start ```shell npx tsx server.ts ``` ## NestJS example ### Install ```shell npm i @arcjet/nest ``` ### Configure Update `src/main.ts`: ```ts import { ArcjetGuard, ArcjetModule, detectBot, fixedWindow, shield, } from "@arcjet/nest"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { APP_GUARD, NestFactory } from "@nestjs/core"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, }), ArcjetModule.forRoot({ isGlobal: true, key: process.env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), fixedWindow({ mode: "LIVE", window: "60s", max: 100, }), ], }), ], controllers: [], providers: [ { provide: APP_GUARD, useClass: ArcjetGuard, }, ], }) class AppModule {} async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); ``` Note: NestJS uses `ArcjetModule.forRoot()` for configuration and `ArcjetGuard` as a global guard. For per-route protection, implement custom guards instead. ### Start ```shell npm run start:dev ``` ## Nuxt example ### Install ```shell npx nuxt module add @arcjet/nuxt ``` This automatically installs and configures the Arcjet Nuxt integration. ### Configure Create a server route at `server/api/protected.get.ts`: ```ts import arcjetNuxt, { detectBot, shield, tokenBucket } from "#arcjet"; const arcjet = arcjetNuxt({ rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); export default defineEventHandler(async (event) => { const decision = await arcjet.protect(event, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { throw createError({ statusCode: 429, statusMessage: "Too Many Requests", }); } if (decision.reason.isBot()) { throw createError({ statusCode: 403, statusMessage: "No bots allowed", }); } throw createError({ statusCode: 403, statusMessage: "Forbidden", }); } return { message: "Hello world" }; }); ``` Note: Nuxt imports from the `#arcjet` virtual module (not a package name). The ARCJET_KEY is set in your `nuxt.config.ts` via the module options. Nuxt passes the `event` object to `protect()`. ### Start ```shell npm run dev ``` ## Remix example ### Install ```shell npm i @arcjet/remix @arcjet/inspect ``` ### Configure Create a route at `app/routes/arcjet.tsx`: ```tsx import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/remix"; import type { LoaderFunctionArgs } from "@remix-run/node"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); export async function loader(args: LoaderFunctionArgs) { const decision = await aj.protect(args, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { throw new Response("Too many requests", { status: 429 }); } else if (decision.reason.isBot()) { throw new Response("Bots forbidden", { status: 403 }); } else { throw new Response("Forbidden", { status: 403 }); } } return null; } export default function Index() { return

Hello world

; } ``` Note: Remix passes the `args` (LoaderFunctionArgs or ActionFunctionArgs) to `protect()`. ### Start ```shell npm run dev ``` ## React Router example ### Install ```shell npm i @arcjet/react-router @arcjet/inspect ``` ### Configure Create a route at `app/routes/home.tsx`: ```tsx import arcjetReactRouter, { detectBot, shield, tokenBucket, } from "@arcjet/react-router"; import type { Route } from "../routes/+types/home"; const arcjet = arcjetReactRouter({ key: process.env.ARCJET_KEY!, rules: [ detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), shield({ mode: "LIVE" }), tokenBucket({ mode: "LIVE", capacity: 10, interval: 10, refillRate: 5, }), ], }); export async function loader(args: Route.LoaderArgs) { const decision = await arcjet.protect(args, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { throw new Response("Too many requests", { status: 429 }); } else if (decision.reason.isBot()) { throw new Response("Bots forbidden", { status: 403 }); } else { throw new Response("Forbidden", { status: 403 }); } } return undefined; } export default function Home() { return

Hello world

; } ``` Note: React Router passes loader/action `args` to `protect()`. ### Start ```shell npm run dev ``` ## Astro example ### Install ```shell npx astro add @arcjet/astro ``` ### Configure Update `astro.config.mjs`: ```js import { defineConfig } from "astro/config"; import node from "@astrojs/node"; import arcjet, { shield, detectBot, tokenBucket } from "@arcjet/astro"; export default defineConfig({ adapter: node({ mode: "standalone" }), integrations: [ arcjet({ rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", ], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }), ], }); ``` Create an API route at `src/pages/api.json.ts`: ```ts export const prerender = false; import type { APIRoute } from "astro"; import aj from "arcjet:client"; export const GET: APIRoute = async ({ request }) => { const decision = await aj.protect(request, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return Response.json({ error: "Too Many Requests" }, { status: 429 }); } else if (decision.reason.isBot()) { return Response.json({ error: "No bots allowed" }, { status: 403 }); } else { return Response.json({ error: "Forbidden" }, { status: 403 }); } } return Response.json({ message: "Hello world" }); }; ``` Note: Astro imports the Arcjet client from the `arcjet:client` virtual module. The ARCJET_KEY is set in your environment variables. The Astro adapter must be configured for server-side rendering. ### Start ```shell npm run dev ``` ## Node.js + Hono example ### Install ```shell npm i @arcjet/node @arcjet/inspect @hono/node-server hono ``` ### Configure ```ts // index.ts import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/node"; import { serve, type HttpBindings } from "@hono/node-server"; import { Hono } from "hono"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: ["CATEGORY:SEARCH_ENGINE"], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); const app = new Hono<{ Bindings: HttpBindings }>(); app.get("/", async (c) => { const decision = await aj.protect(c.env.incoming, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return c.json({ error: "Too Many Requests" }, 429); } else if (decision.reason.isBot()) { return c.json({ error: "No Bots Allowed" }, 403); } else { return c.json({ error: "Forbidden" }, 403); } } return c.json({ message: "Hello Hono!" }); }); serve({ fetch: app.fetch, port: 3000 }); ``` Note: With Hono on Node.js, pass `c.env.incoming` (the Node.js IncomingMessage) to `protect()`, not `c.req`. ## Bun + Hono example ### Install ```shell bun add @arcjet/bun @arcjet/inspect hono ``` ### Configure ```ts // index.ts import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/bun"; import { Hono } from "hono"; import { env } from "bun"; const aj = arcjet({ key: env.ARCJET_KEY!, rules: [ shield({ mode: "LIVE" }), detectBot({ mode: "LIVE", allow: ["CATEGORY:SEARCH_ENGINE"], }), tokenBucket({ mode: "LIVE", refillRate: 5, interval: 10, capacity: 10, }), ], }); const app = new Hono(); app.get("/", async (c) => { const decision = await aj.protect(c.req.raw, { requested: 5 }); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return c.json({ error: "Too many requests" }, 429); } else if (decision.reason.isBot()) { return c.json({ error: "No bots allowed" }, 403); } else { return c.json({ error: "Forbidden" }, 403); } } return c.json({ message: "Hello world" }); }); export default { fetch: aj.handler(app.fetch), port: 3000, }; ``` Note: With Hono on Bun, pass `c.req.raw` (the raw Request) to `protect()`, and wrap the fetch handler with `aj.handler()`. ## Python FastAPI example ### Install ```shell pip install arcjet # or with uv: uv add arcjet fastapi uvicorn langchain langchain-openai ``` ### Configure ```python # main.py import os from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from pydantic import BaseModel from arcjet import ( Mode, SensitiveInfoEntityType, arcjet, detect_bot, detect_prompt_injection, detect_sensitive_info, shield, token_bucket, ) app = FastAPI() aj = arcjet( key=os.getenv("ARCJET_KEY"), # Get your key from https://app.arcjet.com rules=[ # Detect prompt injection attacks before they reach your LLM detect_prompt_injection(mode=Mode.LIVE), # Block sensitive data (PII, credit cards) from entering your AI pipeline detect_sensitive_info( mode=Mode.LIVE, deny=[ SensitiveInfoEntityType.CREDIT_CARD_NUMBER, SensitiveInfoEntityType.EMAIL, SensitiveInfoEntityType.PHONE_NUMBER, ], ), # Rate limit by token budget per user token_bucket( characteristics=["userId"], mode=Mode.LIVE, refill_rate=100, interval=60, capacity=1000, ), # Block automated clients and scrapers detect_bot( mode=Mode.LIVE, allow=[], # empty = block all bots ), # Protect against common web attacks (SQLi, XSS, etc.) shield(mode=Mode.LIVE), ], ) class ChatRequest(BaseModel): message: str @app.post("/chat") async def chat(request: Request, body: ChatRequest): userId = "user_123" # Replace with real user ID from session decision = await aj.protect( request, requested=5, characteristics={"userId": userId}, detect_prompt_injection_message=body.message, sensitive_info_value=body.message, ) if decision.is_denied(): status = 429 if decision.reason_v2.type == "RATE_LIMIT" else 403 return JSONResponse({"error": "Denied"}, status_code=status) # Safe to pass body.message to your LLM return {"reply": "Hello!"} ``` ### Start ```shell uvicorn main:app --reload ``` ## Python Flask example ### Install ```shell pip install arcjet flask # or with uv: uv add arcjet flask langchain langchain-openai ``` ### Configure ```python # main.py import os from flask import Flask, jsonify, request from arcjet import ( Mode, SensitiveInfoEntityType, arcjet_sync, detect_bot, detect_prompt_injection, detect_sensitive_info, shield, token_bucket, ) app = Flask(__name__) aj = arcjet_sync( key=os.getenv("ARCJET_KEY"), # Get your key from https://app.arcjet.com rules=[ # Detect prompt injection attacks before they reach your LLM detect_prompt_injection(mode=Mode.LIVE), # Block sensitive data (PII, credit cards) from entering your AI pipeline detect_sensitive_info( mode=Mode.LIVE, deny=[ SensitiveInfoEntityType.CREDIT_CARD_NUMBER, SensitiveInfoEntityType.EMAIL, SensitiveInfoEntityType.PHONE_NUMBER, ], ), # Rate limit by token budget per user token_bucket( characteristics=["userId"], mode=Mode.LIVE, refill_rate=100, interval=60, capacity=1000, ), # Block automated clients and scrapers detect_bot( mode=Mode.LIVE, allow=[], # empty = block all bots ), # Protect against common web attacks (SQLi, XSS, etc.) shield(mode=Mode.LIVE), ], ) @app.post("/chat") def chat(): userId = "user_123" # Replace with real user ID from session body = request.get_json() message = body.get("message", "") if body else "" decision = aj.protect( request, requested=5, characteristics={"userId": userId}, detect_prompt_injection_message=message, sensitive_info_value=message, ) if decision.is_denied(): status = 429 if decision.reason_v2.type == "RATE_LIMIT" else 403 return jsonify(error="Denied"), status # Safe to pass message to your LLM return jsonify(reply="Hello!") if __name__ == "__main__": app.run(debug=True) ``` Note: Flask uses `arcjet_sync` (synchronous) instead of `arcjet` (async). ### Start ```shell flask run # or: uv run flask run ``` ## Rule parameter reference Every rule accepts `mode: "LIVE" | "DRY_RUN"`. In `DRY_RUN` mode the rule evaluates and returns a decision but never blocks. Use `DRY_RUN` for testing. ### shield(options) Protects against common web attacks (SQL injection, XSS, etc.). ```ts shield({ mode: "LIVE", // or "DRY_RUN" }) ``` Parameters: - `mode` (optional): `"LIVE"` (default) or `"DRY_RUN"` Python: `shield(mode=Mode.LIVE)` ### detectBot(options) Detects and blocks automated clients. ```ts detectBot({ mode: "LIVE", // Use allow OR deny (mutually exclusive) allow: [ "CATEGORY:SEARCH_ENGINE", // Google, Bing, etc "CATEGORY:MONITOR", // Uptime monitoring "CATEGORY:PREVIEW", // Link previews (Slack, Discord) // Or specific bots: "GOOGLEBOT", "BINGBOT", etc. ], // OR: // deny: ["CATEGORY:DEFINITELY_AUTOMATED"], }) ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `allow` (array): Bots/categories to allow — everything else is denied - `deny` (array): Bots/categories to deny — everything else is allowed - `allow` and `deny` are mutually exclusive; use one or the other Bot categories use the `CATEGORY:` prefix. Full list: https://arcjet.com/bot-list Python: `detect_bot(mode=Mode.LIVE, allow=[BotCategory.SEARCH_ENGINE])` or `detect_bot(mode=Mode.LIVE, allow=["CURL"])`. Use `BotCategory.` for categories or pass specific bot name strings directly. ### tokenBucket(options) Token bucket rate limiting. Tokens refill at a steady rate. Best for AI cost control where each request consumes a variable number of tokens. ```ts tokenBucket({ mode: "LIVE", characteristics: ["userId"], // Optional. Defaults to IP-based tracking refillRate: 2_000, // Tokens added per interval interval: "1h", // Refill interval (number in seconds, or string: "1s", "1m", "1h", "1d") capacity: 5_000, // Maximum tokens the bucket can hold }) ``` At protect() time, pass `requested` to deduct tokens: ```ts const decision = await aj.protect(req, { requested: 50 }); ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `characteristics` (optional): Array of strings for tracking (default: IP) - `refillRate` (required): Number of tokens to add per interval - `interval` (required): Seconds (number) or duration string (`"1h"`, `"10m"`) - `capacity` (required): Maximum tokens in the bucket Python: `token_bucket(mode=Mode.LIVE, refill_rate=100, interval=60, capacity=1000, characteristics=["userId"])` The `interval` parameter accepts seconds as a number in Python. ### fixedWindow(options) Fixed window rate limiting. Counts requests in non-overlapping time windows. ```ts fixedWindow({ mode: "LIVE", characteristics: ["userId"], // Optional window: "60s", // Window duration (string: "1s", "10s", "1m", "1h", "1d") max: 100, // Maximum requests per window }) ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `characteristics` (optional): Array of strings for tracking (default: IP) - `window` (required): Duration string (`"60s"`, `"1h"`, etc.) - `max` (required): Maximum requests allowed per window Python: `fixed_window(mode=Mode.LIVE, window=60, max=100)` — `window` takes seconds as a number in Python. ### slidingWindow(options) Sliding window rate limiting. Smooths out the edges of fixed windows. ```ts slidingWindow({ mode: "LIVE", characteristics: ["userId"], // Optional interval: 60, // Window size in seconds max: 100, // Maximum requests per window }) ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `characteristics` (optional): Array of strings for tracking (default: IP) - `interval` (required): Window size in seconds (number) - `max` (required): Maximum requests allowed per window Python: `sliding_window(mode=Mode.LIVE, interval=60, max=100)` — `interval` takes seconds as a number. ### sensitiveInfo(options) Detects and blocks requests containing sensitive information (PII). ```ts sensitiveInfo({ mode: "LIVE", // Use allow OR deny (mutually exclusive) deny: ["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER", "IP_ADDRESS"], // OR: allow: ["EMAIL"], // Only allow email, block everything else }) ``` At protect() time, pass the text to scan: ```ts const decision = await aj.protect(req, { sensitiveInfoValue: "text to scan for PII", }); ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `deny` (array): Entity types to block - `allow` (array): Entity types to allow (blocks everything else) - `deny` and `allow` are mutually exclusive - `contextWindowSize` (optional): Number of tokens for detection context (default: 1) - `detect` (optional): Custom detection function `(tokens: string[]) => Array` Valid entity types: `CREDIT_CARD_NUMBER`, `EMAIL`, `PHONE_NUMBER`, `IP_ADDRESS` Python: `detect_sensitive_info(mode=Mode.LIVE, deny=[SensitiveInfoEntityType.EMAIL, SensitiveInfoEntityType.CREDIT_CARD_NUMBER])` At protect() time: `sensitive_info_value="text to scan"` ### detectPromptInjection(options) Detects prompt injection attacks in user messages before they reach your AI model. ```ts detectPromptInjection({ mode: "LIVE", }) ``` At protect() time, pass the message to scan: ```ts const decision = await aj.protect(req, { detectPromptInjectionMessage: userMessage, }); ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` Python: `detect_prompt_injection(mode=Mode.LIVE)` with `detect_prompt_injection_message=message` at protect() time. ### validateEmail(options) Validates email addresses for signup forms. ```ts validateEmail({ mode: "LIVE", deny: ["DISPOSABLE", "NO_MX_RECORDS", "INVALID"], // OR: allow: ["FREE"], }) ``` At protect() time, pass the email: ```ts const decision = await aj.protect(req, { email: "user@example.com" }); ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `deny` (array): Email types to reject - `allow` (array): Email types to allow (rejects everything else) - `requireTopLevelDomain` (optional): Require a TLD (default: `true`) - `allowDomainLiteral` (optional): Allow domain literals like `[127.0.0.1]` (default: `false`) Valid email types: `DISPOSABLE`, `FREE`, `NO_MX_RECORDS`, `NO_GRAVATAR`, `INVALID` Python: `validate_email(mode=Mode.LIVE, deny=[EmailType.DISPOSABLE, EmailType.INVALID, EmailType.NO_MX_RECORDS])` At protect() time: `email="user@example.com"` ### protectSignup(options) Combined rule for signup form protection (bot detection + email validation + rate limiting). ```ts protectSignup({ email: { mode: "LIVE", deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], }, bots: { mode: "LIVE", deny: ["CATEGORY:DEFINITELY_AUTOMATED"], }, rateLimit: { mode: "LIVE", characteristics: ["ip.src"], interval: 600, max: 5, }, }) ``` At protect() time, pass the email: ```ts const decision = await aj.protect(req, { email: "user@example.com" }); ``` ### filter(options) Filter requests based on expressions using request and IP metadata. ```ts filter({ mode: "LIVE", // Use allow OR deny (mutually exclusive) deny: ["ip.src.vpn", "ip.src.tor"], // OR: allow: ['ip.src.country eq "US"'], }) ``` Parameters: - `mode` (optional): `"LIVE"` or `"DRY_RUN"` - `deny` (array): Expressions that cause a DENY when matched - `allow` (array): Expressions that cause an ALLOW when matched (denies everything else) - Maximum 10 expressions per rule, each max 1024 bytes Available fields include: `http.host`, `http.request.method`, `http.request.uri.path`, `ip.src`, `ip.src.country`, `ip.src.vpn`, `ip.src.tor`, `ip.src.hosting`, `ip.src.proxy`, and many more. Python: `filter_request(mode=Mode.LIVE, deny=["ip.src.vpn", "ip.src.tor"])` Custom local fields: pass `filter_local={"key": "value"}` at protect() time, then reference as `local.key` in expressions. ## Decision API reference `protect()` returns a decision object with these methods: ### Conclusion methods ```ts const decision = await aj.protect(req); decision.isDenied() // true if any LIVE rule triggered a DENY decision.isAllowed() // true if all rules passed decision.isErrored() // true if there was an error evaluating rules decision.isChallenged() // true if a challenge is required ``` ### Reason methods (check WHY a request was denied) ```ts if (decision.isDenied()) { decision.reason.isRateLimit() // Rate limit exceeded decision.reason.isBot() // Bot detected decision.reason.isShield() // Shield WAF triggered decision.reason.isSensitiveInfo() // PII detected decision.reason.isEmail() // Email validation failed decision.reason.isPromptInjection() // Prompt injection detected decision.reason.isFilterRule() // Filter rule matched } ``` ### Error handling ```ts if (decision.isErrored()) { // Arcjet fails open — log the error and allow the request console.error("Arcjet error", decision.reason.message); } ``` ### IP analysis (available on every decision) ```ts decision.ip.isHosting() // true if from a hosting/cloud provider decision.ip.isVpn() // true if from a VPN decision.ip.isTor() // true if from Tor decision.ip.isProxy() // true if from a proxy decision.ip.isRelay() // true if from a relay ``` ### Rate limit metadata ```ts // Available when a rate limit rule is configured for (const result of decision.results) { if (result.reason.isRateLimit()) { result.reason.max // Configured maximum result.reason.remaining // Requests/tokens remaining result.reason.window // Total window in seconds result.reason.reset // Seconds until window resets } } ``` ### Python decision API ```python # Top-level checks decision.is_denied() # True if any rule denied the request decision.is_allowed() # True if all rules allowed the request decision.is_error() # True if Arcjet encountered an error (fails open) # reason_v2.type values: "BOT", "RATE_LIMIT", "SHIELD", "EMAIL", "ERROR", "FILTER" if decision.reason_v2.type == "RATE_LIMIT": print(decision.reason_v2.remaining) # tokens/requests remaining elif decision.reason_v2.type == "BOT": print(decision.reason_v2.denied) # list of denied bot names # Per-rule results (for granular handling) for result in decision.results: print(result.reason_v2.type, result.is_denied()) # IP helpers (same as JS) decision.ip.is_hosting() decision.ip.is_vpn() decision.ip.is_tor() decision.ip.is_proxy() ``` ### Python protect() parameters All parameters are optional keyword arguments passed alongside `request`: | Parameter | Type | Used by | | --------------------------------- | ---------------- | -------------------------- | | `requested` | `int` | Token bucket rate limit | | `characteristics` | `dict[str, Any]` | Rate limiting | | `detect_prompt_injection_message` | `str` | Prompt injection detection | | `sensitive_info_value` | `str` | Sensitive info detection | | `email` | `str` | Email validation | | `filter_local` | `dict[str, str]` | Request filters | | `ip_src` | `str` | Manual IP override | ## withRule() pattern — reusing a single client Create one Arcjet instance and add route-specific rules with `withRule()`: ```ts // lib/arcjet.ts — create and export a base instance import arcjet, { detectBot, fixedWindow, sensitiveInfo, shield, } from "@arcjet/next"; export { detectBot, fixedWindow, sensitiveInfo, shield }; export default arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Base rules that apply to every route (optional) ], }); ``` ```ts // app/api/chat/route.ts — add route-specific rules import arcjet, { detectBot, fixedWindow } from "@/lib/arcjet"; const aj = arcjet .withRule( detectBot({ mode: "LIVE", allow: [], }), ) .withRule( fixedWindow({ mode: "LIVE", max: 100, window: "60s", }), ); export async function GET(req: Request) { const decision = await aj.protect(req); // ... } ``` ## Best practices and anti-patterns ### Do - Create the Arcjet client ONCE, outside the request handler, and reuse it. - Call `protect()` inside the route handler where you have the full request context. - Start new rules in `DRY_RUN` mode, verify in the dashboard, then switch to `LIVE`. - Handle all denial reasons explicitly (rate limit, bot, shield, etc.). - Use `withRule()` to attach route-specific rules to a shared base instance. ### Don't - Don't create a new Arcjet instance per request — this defeats caching. - Don't call `protect()` multiple times for the same request — it may double- count rate limits. - Don't use Arcjet in middleware — middleware lacks route context. Call `protect()` in each route handler instead. If you must use middleware, scope it carefully and don't also call `protect()` in the route handler. - Don't pass personal information (email addresses, names) as rate limit `characteristics` — use opaque identifiers like user IDs. - Next.js renamed `middleware.js` to `proxy.js` in Next.js 16. The Arcjet `protect()` function should be called only once per request. ### Proxies and load balancers If your app is behind a proxy or load balancer, configure Arcjet to see the real client IP: ```ts const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [], proxies: [ "100.100.100.100", // A single IP "100.100.100.0/24", // A CIDR range ], }); ``` This is not needed on Firebase, Netlify, Fly.io, or Vercel — Arcjet auto-detects proxy IPs on these platforms. ## Product philosophy 1. Enforcement runs inline in your application — with access to identity, route, session, and spend context no proxy can see. 2. Your agent configures protections via MCP. You review and approve. 3. Start in DRY_RUN, verify against real traffic, promote to LIVE. 4. Remote rules let you and your agents respond to attacks immediately — no code deployment needed. Find out more at https://docs.arcjet.com/architecture ## Important notes - Arcjet runs server-side and does not require any client-side integration. - Arcjet is a paid service. See https://arcjet.com/pricing for details. - Review https://docs.arcjet.com/best-practices for best practices. - Calls to `protect()` never throw. Arcjet fails open so that a service issue or misconfiguration does not block all requests. ## Reference guides ### Features - [Shield](https://docs.arcjet.com/shield) - [Rate limiting](https://docs.arcjet.com/rate-limiting) - [Bot protection](https://docs.arcjet.com/bot-protection) - [Email validation](https://docs.arcjet.com/email-validation) - [Sensitive information](https://docs.arcjet.com/sensitive-info) - [Prompt injection](https://docs.arcjet.com/prompt-injection) - [Signup form protection](https://docs.arcjet.com/signup-protection) - [Filters](https://docs.arcjet.com/filters) ### SDKs - [Astro](https://docs.arcjet.com/reference/astro) - [Bun](https://docs.arcjet.com/reference/bun) - [Deno](https://docs.arcjet.com/reference/deno) - [Fastify](https://docs.arcjet.com/reference/fastify) - [NestJS](https://docs.arcjet.com/reference/nestjs) - [Next.js](https://docs.arcjet.com/reference/nextjs) - [Node.js](https://docs.arcjet.com/reference/nodejs) - [Nuxt](https://docs.arcjet.com/reference/nuxt) - [React Router](https://docs.arcjet.com/reference/react-router) - [Remix](https://docs.arcjet.com/reference/remix) - [SvelteKit](https://docs.arcjet.com/reference/sveltekit) ## Support See the troubleshooting guide at https://docs.arcjet.com/troubleshooting. For help, email or [join the Discord server](https://arcjet.com/discord).