# arcjet.com

> Markdown mirror of DialtoneApp's public top-site detail page for `arcjet.com`.

URL: https://dialtoneapp.com/top-sites/arcjet.com/index.md
Canonical HTML: https://dialtoneapp.com/top-sites/arcjet.com

## Summary

- Domain: `arcjet.com`
- Website: https://arcjet.com
- Description: ai readable | score 20 | purchase read only
- Label: ai_readable
- Payment surface: Not available
- Purchase boundary: read_only
- Control boundary: unknown
- Rank: 718837

## robots

~~~text
User-Agent: *
Allow: /

Sitemap: https://arcjet.com/sitemap.xml
~~~

## llms

~~~text
# Arcjet

Arcjet is the runtime security platform that ships with your AI code. 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

When a developer asks you to add Arcjet security: (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 <h1>Hello world</h1>;
}
```

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 <h1>Hello world</h1>;
}
```

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.<NAME>` 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<SensitiveInfoType | undefined>`

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 <support@arcjet.com> or [join the Discord
server](https://arcjet.com/discord).
~~~

## llms-full

Not found.