Arcjet Next.js SDK reference
This is the reference guide for the Arcjet Next.js SDK, available on GitHub and licensed under the Apache 2.0 license.
What is Arcjet?
Arcjet helps developers protect their apps in just a few lines of code. Bot detection. Rate limiting. Email validation. Attack protection. Data redaction. A developer-first approach to security.Installation
In your project root, run the following command to install the SDK:
npm i @arcjet/next
pnpm add @arcjet/next
yarn add @arcjet/next
Requirements
- Next.js 14 or 15.
- CommonJS is not supported. Arcjet is ESM only.
Quick start
Check out the quick start guide.
Configuration
Create a new Arcjet
object with your API key and rules. This should be outside
of the request handler.
The required fields are:
key
(string
) - Your Arcjet site key. This can be found in the SDK Installation section for the site in the Arcjet Dashboard.rules
- The rules to apply to the request. See the various sections of the docs for how to configure these e.g. shield, rate limiting, bot protection, email validation.
The optional fields are:
characteristics
(string[]
) - A list of characteristics to be used to uniquely identify clients.proxies
(string[]
) - A list of one or more trusted proxies. These addresses will be excluded when Arcjet is determining the client IP address. This is useful if you are behind a load balancer or proxy that sets the client IP address in a header. See Load balancers & proxies below for an example.
import arcjet, { shield } from "@arcjet/next";
const aj = arcjet({ // Get your site key from https://app.arcjet.com // and set it as an environment variable rather than hard coding. // See: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
import arcjet, { shield } from "@arcjet/next";
const aj = arcjet({ // Get your site key from https://app.arcjet.com // and set it as an environment variable rather than hard coding. // See: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
Single instance
We recommend creating a single instance of the Arcjet
object and reusing it
throughout your application. This is because the SDK caches decisions and
configuration to improve performance.
The pattern we use is to create a utility file that exports the Arcjet
object
and then import it where you need it. See our example Next.js
app for how
this is done.
Rule modes
Each rule can be configured in either LIVE
or DRY_RUN
mode. When in
DRY_RUN
mode, each rule will return its decision, but the end conclusion will
always be ALLOW
.
This allows you to run Arcjet in passive / demo mode to test rules before enabling them.
import arcjet, { fixedWindow } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, characteristics: ["ip.src"], rules: [ fixedWindow( // This rule is live { mode: "LIVE", window: "1h", max: 60, }, // This rule is in dry run mode, so will log but not block { mode: "DRY_RUN", characteristics: ['http.request.headers["x-api-key"]'], window: "1h", // max could also be a dynamic value applied after looking up a limit // elsewhere e.g. in a database for the authenticated user max: 600, }, ), ],});
As the top level conclusion will always be ALLOW
in DRY_RUN
mode, you can
loop through each rule result to check what would have happened:
for (const result of decision.results) { if (result.isDenied()) { console.log("Rule returned deny conclusion", result); }}
Multiple rules
You can combine rules to create a more complex protection strategy. For example, you can combine rate limiting and bot protection rules to protect your API from automated clients.
import arcjet, { detectBot, tokenBucket } from "@arcjet/next";
// Create an Arcjet instance with multiple rulesconst aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ tokenBucket({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only refillRate: 5, // refill 5 tokens per interval interval: 10, // refill every 10 seconds capacity: 10, // bucket maximum capacity of 10 tokens }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
import arcjet, { detectBot, tokenBucket } from "@arcjet/next";
// Create an Arcjet instance with multiple rulesconst aj = arcjet({ key: process.env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ tokenBucket({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only refillRate: 5, // refill 5 tokens per interval interval: 10, // refill every 10 seconds capacity: 10, // bucket maximum capacity of 10 tokens }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
Environment variables
The following environment variables can be used to configure the SDK at runtime:
ARCJET_BASE_URL
- Will override the decision API which the SDK communicates with. This defaults tohttps://decide.arcjet.com
and should only be changed if directed by Arcjet support.ARCJET_LOG_LEVEL
- The log level to use, eitherdebug
,info
,warn
, orerror
. Defaults towarn
. If a rule is in dry run mode, a warning will be output with the decision that would have been applied.ARCJET_ENV
- Set todevelopment
to force Arcjet into development mode. This will allow private/internal addresses so that the SDKs work correctly locally. You usually do not need to set this because it usesNODE_ENV
when set. See Troubleshooting for when this may be needed.
Custom logging
The SDK uses a lightweight logger which mirrors the Pino structured logger interface. You can use this to customize the logging output.
First, install the required packages:
npm install pino pino-pretty
Then, create a custom logger that will log to JSON in production and pretty print in development:
import arcjet, { shield } from "@arcjet/next";import pino, { type Logger } from "pino";
const logger: Logger = process.env.NODE_ENV !== "development" ? // JSON in production, default to warn pino({ level: process.env.ARCJET_LOG_LEVEL || "warn" }) : // Pretty print in development, default to debug pino({ transport: { target: "pino-pretty", options: { colorize: true, }, }, level: process.env.ARCJET_LOG_LEVEL || "debug", });
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ], // Use the custom logger log: logger,});
import arcjet, { shield } from "@arcjet/next";import pino from "pino";
const logger = process.env.NODE_ENV !== "development" ? // JSON in production, default to warn pino({ level: process.env.ARCJET_LOG_LEVEL || "warn" }) : // Pretty print in development, default to debug pino({ transport: { target: "pino-pretty", options: { colorize: true, }, }, level: process.env.ARCJET_LOG_LEVEL || "debug", });
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ], // Use the custom logger log: logger,});
Finally, Next.js requires marking Pino as an external package in your Next.js config file:
/** @type {import('next').NextConfig} */const nextConfig = { reactStrictMode: true, experimental: { // https://github.com/vercel/next.js/discussions/46987#discussioncomment-8464812 serverComponentsExternalPackages: ["pino", "pino-pretty"], },};
module.exports = nextConfig;
Load balancers & proxies
If your application is behind a load balancer, Arcjet will only see the IP address of the load balancer and not the real client IP address.
To fix this, most load balancers will set the X-Forwarded-For
header with the
real client IP address plus a list of proxies that the request has passed
through.
The problem with is that the X-Forwarded-For
header can be spoofed by the
client, so you should only trust it if you are sure that the load balancer is
setting it correctly. See the MDN
docs
for more details.
You can configure Arcjet to trust IP addresses in the X-Forwarded-For
header
by setting the proxies
field in the configuration. This should be a list of
the IP addresses of your load balancers to be removed, so that the last IP
address in the list is the real client IP address.
Example
For example, if the load balancer is at 100.100.100.100
and the client IP
address is 192.168.1.1
, the X-Forwarded-For
header will be:
X-Forwarded-For: 192.168.1.1, 100.100.100.100
You should set the proxies
field to ["100.100.100.100"]
so Arcjet will use
192.168.1.1
as the client IP address.
import arcjet from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [], proxies: ["100.100.100.100"],});
Protect
Arcjet provides a single protect
function that is used to execute your
protection rules. This requires a request
object which is the request
argument as passed to the Next.js request handler. Rules you add to the SDK may
require additional details, such as the validateEmail
rule requiring an
additional email
prop.
This function returns a Promise
that resolves to an ArcjetDecision
object,
which provides a high-level conclusion and detailed explanations of the decision
made by Arcjet.
import arcjet, { shield } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export async function POST(req: Request) { const decision = await aj.protect(req);
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { shield } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req);
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}
import arcjet, { shield } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export async function POST(req) { const decision = await aj.protect(req);
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { shield } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req);
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}
Pages / Page components
Arcjet can protect Next.js pages that are server components (the default). Client components cannot be protected because they run on the client side and do not have access to the request object.
// Pages are server components by default, so this is just being explicit"use server";
import arcjet, { detectBot, request } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com characteristics: ["ip.src"], // Track requests by IP rules: [ // Create a bot detection rule detectBot({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only // Block all bots except the following allow: [ "CATEGORY:SEARCH_ENGINE", // Google, Bing, etc // Uncomment to allow these other common bot categories // See the full list at https://arcjet.com/bot-list //"CATEGORY:MONITOR", // Uptime monitoring services //"CATEGORY:PREVIEW", // Link previews e.g. Slack, Discord ], }), ],});
export default async function Page() { // Access the request object so Arcjet can analyze it const req = await request(); // Call Arcjet protect const decision = await aj.protect(req);
if (decision.isDenied()) { // This will be caught by the nearest error boundary // See https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#error-handling throw new Error("Forbidden"); }
return <h1>Hello, Home page!</h1>;}
Server actions
Arcjet supports server actions in both Next.js 14 and 15 for all features except
sensitive data detection. You need to call a utility function request()
that
accesses the headers we need to analyze the request.
For example:
Client component example
In this example the server action is passed as a prop to a client component.
"use server";
import arcjet, { detectBot, request, shield } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ // Shield protects your app from common attacks e.g. SQL injection shield({ mode: "LIVE" }), // Create a bot detection rule detectBot({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only // Block all bots. See // https://arcjet.com/bot-list allow: [], }), ],});
export async function create() { // Access request data that Arcjet needs when you call `protect()` similarly // to `await headers()` and `await cookies()` in `next/headers` const req = await request();
// Call Arcjet protect const decision = await aj.protect(req); console.log("Decision:", decision);
if (decision.isDenied()) { // This will be caught by the nearest error boundary // See https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#error-handling throw new Error("Forbidden"); }
// mutate data}
"use client";
import { create } from "@/app/actions";
export function Button() { return <button onClick={() => create()}>Create</button>;}
import { Button } from "./ui/button";
export default function Home() { return ( <div> <Button /> </div> );}
Form example
In this example the server action is handling a form submission, based on the Next.js example form handler.
These examples will throw
when there is an error
(docs),
but you can use
useFormState
to return errors to the form. If you are using React 19, use
useActionState
instead.
import arcjet, { shield, request, detectBot } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ // Shield protects your app from common attacks e.g. SQL injection shield({ mode: "LIVE" }), // Create a bot detection rule detectBot({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only // Block all bots. See // https://arcjet.com/bot-list allow: [], }), ],});
// A simple form handler Based on// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#formsexport default function Page() { async function createInvoice(formData: FormData) { "use server";
// Access the request object so Arcjet can analyze it const req = await request(); // Call Arcjet protect const decision = await aj.protect(req);
if (decision.isDenied()) { // This will be caught by the nearest error boundary // See https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#error-handling throw new Error("Forbidden"); }
const rawFormData = { customerId: formData.get("customerId"), amount: formData.get("amount"), status: formData.get("status"), };
// mutate data // revalidate cache }
return <form action={createInvoice}>...</form>;}
Decision
The protect
function function returns a Promise
that resolves to an
ArcjetDecision
object. This contains the following properties:
id
(string
) - The unique ID for the request. This can be used to look up the request in the Arcjet dashboard. It is prefixed withreq_
for decisions involving the Arcjet cloud API. For decisions taken locally, the prefix islreq_
.conclusion
("ALLOW" | "DENY" | "CHALLENGE" | "ERROR"
) - The final conclusion based on evaluating each of the configured rules. If you wish to accept Arcjet’s recommended action based on the configured rules then you can use this property.reason
(ArcjetReason
) - An object containing more detailed information about the conclusion.results
(ArcjetRuleResult[]
) - An array ofArcjetRuleResult
objects containing the results of each rule that was executed.ttl
(uint32
) - The time-to-live for the decision in seconds. This is the time that the decision is valid for. After this time, the decision will be re-evaluated. The SDK automatically cachesDENY
decisions for the length of the TTL.ip
(ArcjetIpDetails
) - An object containing Arcjet’s analysis of the client IP address. See IP analysis below for more information.
Conclusion
The ArcjetDecision
object has the following methods that should be used to
check the conclusion:
isAllowed()
(bool
) - The request should be allowed.isDenied()
(bool
) - The request should be denied.isErrored()
(bool
) - There was an unrecoverable error.
The conclusion will be the highest-severity finding when evaluating the configured
rules. "DENY"
is the highest severity, followed by "CHALLENGE"
, then "ERROR"
and
finally "ALLOW"
as the lowest severity.
For example, when a bot protection rule returns an error and a validate email
rule returns a deny, the overall conclusion would be deny. To access the error
you would have to use the results
property on the decision.
Reason
The reason
property of the ArcjetDecision
object contains an ArcjetReason
object which provides more detailed information about the conclusion. This is
the final decision reason and is based on the configured rules.
The ArcjetReason
object has the following methods that can be used to check
which rule caused the conclusion:
It will always be the highest-priority rule that produced that conclusion,
to inspect other rules consider iterating over the results
property on the decision.
isBot()
(bool
) - Returnstrue
if the bot protection rules have been applied and the request was considered to have been made by a bot.isEmail()
(bool
) - Returnstrue
if the email rules have been applied and the email address has a problem.isRateLimit()
(bool
) - Returnstrue
if the rate limit rules have been applied and the request has exceeded the rate limit.isSensitiveInfo()
(bool
) - Returnstrue
if sensitive info rules have been applied and sensitive info has been detected.isShield()
(bool
) - Returnstrue
if the shield rules have been applied and the request is suspicious based on analysis by Arcjet Shield WAF.isError()
(bool
) - Returnstrue
if there was an error processing the request.
Results
The results
property of the ArcjetDecision
object contains an array of
ArcjetRuleResult
objects. There will be one for each configured rule so you
can inspect the individual results:
id
(string
) - The ID of the rule result. Not yet implemented.state
(ArcjetRuleState
) - Whether the rule was executed or not.conclusion
(ArcjetConclusion
) - The conclusion of the rule. This will be one of the above conclusions:ALLOW
,DENY
,CHALLENGE
, orERROR
.reason
(ArcjetReason
) - An object containing more detailed information about the conclusion for this rule. Each rule type has its own reason object with different properties.
You can iterate through the results and check the conclusion for each rule.
for (const result of decision.results) { console.log("Rule Result", result);}
This example will log the full result as well as each rate limit rule:
import arcjet, { fixedWindow, detectBot } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ fixedWindow({ mode: "LIVE", window: "1h", max: 60, }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(req: Request) { const decision = await aj.protect(req);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isRateLimit()) { console.log("Rate limit rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { fixedWindow, detectBot } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ fixedWindow({ mode: "LIVE", window: "1h", max: 60, }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req); console.log("Decision", decision);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isRateLimit()) { console.log("Rate limit rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}
import arcjet, { fixedWindow, detectBot } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ fixedWindow({ mode: "LIVE", window: "1h", max: 60, }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(req) { const decision = await aj.protect(req);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isRateLimit()) { console.log("Rate limit rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { fixedWindow, detectBot } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ fixedWindow({ mode: "LIVE", window: "1h", max: 60, }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req); console.log("Decision", decision);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isRateLimit()) { console.log("Rate limit rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}
Rule state
The state
property of the ArcjetRuleResult
object is an ArcjetRuleState
.
Each rule is evaluated individually and can be in one of the following states:
DRY_RUN
- The rule was executed in dry run mode. This means that the rule was executed but the conclusion was not applied to the request. This is useful for testing rules before enabling them.RUN
- The rule was executed and the conclusion was applied to the request.NOT_RUN
- The rule was not executed. This can happen if another rule has already reached a conclusion that applies to the request. For example, if a rate limit rule is configured then these are evaluated before all other rules. If the client has reached the maximum number of requests then other rules will not be evaluated.CACHED
- The rule was not executed because the previous result was cached. Results are cached when the decision conclusion isDENY
. Subsequent requests from the same client will not be evaluated against the rule until the cache expires.
Rule reason
The reason
property of the ArcjetRuleResult
object contains an
ArcjetReason
object which provides more detailed information about the
conclusion for that configured rule.
Shield
The ArcjetReason
object for shield rules has the following properties:
shieldTriggered: boolean;
See the shield documentation for more information about these properties.
Bot protection
The ArcjetReason
object for bot protection rules has the following properties:
allowed: string[];denied: string[];
Each of the allowed
and denied
arrays contains the identifiers of the bots
allowed or denied from our full list of bots.
Rate limiting
The ArcjetReason
object for rate limiting rules has the following properties:
max: number;remaining: number;window: number;reset: number;
See the rate limiting documentation for more information about these properties.
Email validation & verification
The ArcjetReason
object for email rules has the following properties:
emailTypes: ArcjetEmailType[];
An ArcjetEmailType
is one of the following strings:
"DISPOSABLE" | "FREE" | "NO_MX_RECORDS" | "NO_GRAVATAR" | "INVALID";
See the email validation documentation for more information about these properties.
IP analysis
As of SDK version 1.0.0-alpha.11
, the ArcjetDecision
object contains an ip
property. This includes additional data about the client IP address:
IP location
The following are available on the Free plan:
country
(string | undefined
): the country code the client IP address.countryName
(string | undefined
): the country name of the client IP address.
The following are available on the Pro plan and above:
latitude
(number | undefined
): the latitude of the client IP address.longitude
(number | undefined
): the longitude of the client IP address.accuracyRadius
(number | undefined
): how accurate the location is in kilometers.timezone
(string | undefined
): the timezone of the client IP address.postalCode
(string | undefined
): the postal or zip code of the client IP address.city
(string | undefined
): the city of the client IP address.region
(string | undefined
): the region of the client IP address.continent
(string | undefined
): the continent code of the client IP address.continentName
(string | undefined
): the continent name of the client IP address.
The IP location fields may be undefined
, but you can use various methods to
check their availability. Using the methods will also refine the type to remove
the need for null or undefined checks.
hasLatitude()
(bool
): returns whether thelatitude
andaccuracyRadius
fields are available.hasLongitude()
(bool
): returns whether thelongitude
andaccuracyRadius
fields are available.hasAccuracyRadius()
(bool
): returns whether thelongitude
,latitude
, andaccuracyRadius
fields are available.hasTimezone()
(bool
): returns whether thetimezone
field is available.hasPostalCode()
(bool
): returns whether thepostalCode
field is available.hasCity()
(bool
): returns whether thecity
field is available.hasRegion()
(bool
): returns whether theregion
field is available.hasCountry()
(bool
): returns whether thecountry
andcountryName
fields are available.hasContinent()
(bool
): returns whether thecontinent
andcontinentName
fields are available.
Location accuracy
IP geolocation can be notoriously inaccurate, especially for mobile devices,
satellite internet providers, and even just normal users. Likewise with the
specific fields like city
and region
, which can be very inaccurate. Country
is usually accurate, but there are often cases where IP addresses are
mis-located. These fields are provided for convenience e.g. suggesting a user
location, but should not be relied upon by themselves.
IP AS
This is useful for identifying the network operator of the client IP address. This is useful for understanding whether the client is likely to be automated or not, or being stricter with requests from certain networks.
The IP AS fields may be undefined
, but you can use the hasASN()
method to
check their availability. Using this method will also refine the type to remove
the need for null-ish checks.
The following are available on the Pro plan and above:
hasASN()
(bool
): returns whether all of the ASN fields are available.asn
(string | undefined
): the autonomous system (AS) number of the client IP address.asnName
(string | undefined
): the name of the AS of the client IP address.asnDomain
(string | undefined
): the domain of the AS of the client IP address.asnType
('isp' | 'hosting' | 'business' | 'education'
): the type of the AS of the client IP address. Real users are more likely to be on an ISP or business network rather than a hosting provider. Education networks often have a single or small number of IP addresses even though there are many users. A common mistake is to block a single IP because of too many requests when it is a university or company network using NAT (Network Address Translation) to give many users the same IP.asnCountry
(string | undefined
): the country code of the AS of the client IP address. This is the administrative country of the AS, not necessarily the country of the client IP address.
IP type
The service
field may be undefined
, but you can use the hasService()
method to check the availability. Using this method will also refine the type to
remove the need for null-ish checks.
The following are available on all pricing plans:
hasService()
(bool
): whether theservice
field is available.service
(string | undefined
): the name of the service associated with the IP address—e.g.Apple Private Relay
.isHosting()
(bool
): returns whether the IP address of the client is owned by a hosting provider. Requests originating from a hosting provider IP significantly increase the likelihood that this is an automated client.isVpn()
(bool
): returns whether the IP address of the client is owned by a VPN provider. Many people use VPNs for privacy or work purposes, so by itself this is not an indicator of the client being automated. However, it does increase the risk score of the client and depending on your use case it may be a characteristic you wish to restrict.isProxy()
(bool
): returns whether the IP address of the client is owned by a proxy provider. Similar toisVpn()
, but proxies are more likely to involve automated traffic.isTor()
(bool
): returns whether the IP address of the client is known to be part of the Tor network. As withisVpn()
, there are legitimate uses for hiding your identity through Tor, however it is also often a way to hide the origin of malicious traffic.isRelay()
(bool
): returns whether the IP address of the client is owned by a relay service. The most common example is Apple iCloud Relay, which indicates the client is less likely to be automated because Apple requires a paid subscription linked to an Apple account in good standing.
Example
import arcjet, { shield } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export async function POST(req: Request) { const decision = await aj.protect(req);
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
if (decision.ip.hasCountry()) { return NextResponse.json({ message: `Hello ${decision.ip.countryName}!`, country: decision.ip, }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { shield } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req);
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
if (decision.ip.hasCountry()) { return res.status(200).json({ message: `Hello ${decision.ip.countryName}!`, country: decision.ip, }); }
res.status(200).json({ name: "Hello world" });}
import arcjet, { shield } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export async function POST(req) { const decision = await aj.protect(req);
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
if (decision.ip.hasCountry()) { return NextResponse.json({ message: `Hello ${decision.ip.countryName}!`, country: decision.ip, }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { shield } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req);
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
if (decision.ip.hasCountry()) { return res.status(200).json({ message: `Hello ${decision.ip.countryName}!`, country: decision.ip, }); }
res.status(200).json({ name: "Hello world" });}
For the IP address 8.8.8.8
you might get the following response. Only the
fields we have data for will be returned:
{ "name": "Hello United States!", "ip": { "country": "US", "countryName": "United States", "continent": "NA", "continentName": "North America", "asn": "AS15169", "asnName": "Google LLC", "asnDomain": "google.com" }}
Error handling
Arcjet is designed to fail open so that a service issue or misconfiguration does
not block all requests. The SDK will also time out and fail open after 1000ms
when NODE_ENV
or ARCJET_ENV
is development
and 500ms otherwise. However,
in most cases, the response time will be less than 20-30ms.
If there is an error condition when processing the rule, Arcjet will return an
ERROR
result for that rule and you can check the message
property on the rule’s
error result for more information.
If all other rules that were run returned an ALLOW
result, then the final Arcjet
conclusion will be ERROR
.
import arcjet, { slidingWindow } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "LIVE", interval: "1h", max: 60, }), ],});
export async function GET(req: Request) { const decision = await aj.protect(req);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Too Many Requests" }, { status: 429 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { slidingWindow } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "LIVE", interval: "1h", max: 60, }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return res.status(503).json({ error: "Service unavailable" }); } }
if (decision.isDenied()) { return res.status(429).json({ error: "Too Many Requests" }); }
res.status(200).json({ name: "Hello world" });}
import arcjet, { slidingWindow } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "LIVE", interval: "1h", max: 60, }), ],});
export async function GET(req) { const decision = await aj.protect(req);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Too Many Requests" }, { status: 429 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { slidingWindow } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "LIVE", interval: "1h", max: 60, }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return res.status(503).json({ error: "Service unavailable" }); } }
if (decision.isDenied()) { return res.status(429).json({ error: "Too Many Requests" }); }
res.status(200).json({ name: "Hello world" });}
Ad hoc rules
Sometimes it is useful to add additional protection via a rule based on the
logic in your handler; however, you usually want to inherit the rules, cache,
and other configuration from our primary SDK. This can be achieved using the
withRule
function which accepts an ad-hoc rule and can be chained to add
multiple rules. It returns an augmented client with the specialized protect
function.
import arcjet, { detectBot, fixedWindow, shield } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
function getClient(userId?: string) { if (userId) { return aj; } else { // Only apply bot detection and rate limiting to non-authenticated users return ( aj .withRule( fixedWindow({ max: 10, window: "1m", }), ) // You can chain multiple rules, or just use one .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only allow: [], // "allow none" will block all detected bots }), ) ); }}
export async function POST(req: Request) { // This userId is hard coded for the example, but this is where you would do a // session lookup and get the user ID. const userId = "totoro";
const decision = await getClient(userId).protect(req);
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { detectBot, fixedWindow, shield } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
function getClient(userId?: string) { if (userId) { return aj; } else { // Only apply bot detection and rate limiting to non-authenticated users return ( aj .withRule( fixedWindow({ max: 10, window: "1m", }), ) // You can chain multiple rules, or just use one .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only allow: [], // "allow none" will block all detected bots }), ) ); }}
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { // This userId is hard coded for the example, but this is where you would do a // session lookup and get the user ID. const userId = "totoro";
const decision = await getClient(userId).protect(req);
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}
import arcjet, { detectBot, fixedWindow, shield } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
function getClient(userId) { if (userId) { return aj; } else { // Only apply bot detection and rate limiting to non-authenticated users return ( aj .withRule( fixedWindow({ max: 10, window: "1m", }), ) // You can chain multiple rules, or just use one .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only allow: [], // "allow none" will block all detected bots }), ) ); }}
export async function POST(req) { // This userId is hard coded for the example, but this is where you would do a // session lookup and get the user ID. const userId = "totoro";
const decision = await getClient(userId).protect(req);
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}
import arcjet, { detectBot, fixedWindow, shield } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
function getClient(userId) { if (userId) { return aj; } else { // Only apply bot detection and rate limiting to non-authenticated users return ( aj .withRule( fixedWindow({ max: 10, window: "1m", }), ) // You can chain multiple rules, or just use one .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only allow: [], // "allow none" will block all detected bots }), ) ); }}
export default async function handler(req, res) { // This userId is hard coded for the example, but this is where you would do a // session lookup and get the user ID. const userId = "totoro";
const decision = await getClient(userId).protect(req);
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}
IP address detection
Arcjet will automatically detect the IP address of the client making the request based on the context provided. The implementation is open source in our @arcjet/ip package.
In development environments (NODE_ENV === "development"
or
ARCJET_ENV === "development"
), we allow private/internal addresses so that the
SDKs work correctly locally.
Client override
The default client can be overridden. If no client is specified, a default one will be used. Generally you should not need to provide a client - the Arcjet Next.js SDK will automatically handle this for you, including when using the Edge Runtime.
import arcjet, { createRemoteClient, slidingWindow } from "@arcjet/next";import { baseUrl } from "@arcjet/env";
const client = createRemoteClient({ // baseUrl defaults to https://decide.arcjet.com and should only be changed if // directed by Arcjet. It can also be set via the ARCJET_BASE_URL environment // variable. baseUrl: baseUrl(process.env), // timeout is the maximum time to wait for a response from the server. It // defaults to 1000ms when NODE_ENV or ARCJET_ENV is "development" and 500ms // otherwise. This is a conservative limit to fail open by default. In most // cases, the response time will be <20-30ms. timeout: 500,});
const aj = arcjet({ key: process.env.ARCJET_KEY!, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "LIVE", interval: "1h", max: 60, }), ], client,});
import arcjet, { createRemoteClient, slidingWindow } from "@arcjet/next";import { baseUrl } from "@arcjet/env";
const client = createRemoteClient({ // baseUrl defaults to https://decide.arcjet.com and should only be changed if // directed by Arcjet. It can also be set via the ARCJET_BASE_URL environment // variable. baseUrl: baseUrl(process.env), // timeout is the maximum time to wait for a response from the server. It // defaults to 1000ms when NODE_ENV or ARCJET_ENV is "development" and 500ms // otherwise. This is a conservative limit to fail open by default. In most // cases, the response time will be <20-30ms. timeout: 500,});
const aj = arcjet({ key: process.env.ARCJET_KEY, // Tracking by ip.src is the default if not specified //characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "LIVE", interval: "1h", max: 60, }), ], client,});
Version support
Node
Arcjet supports the active and maintenance LTS versions of Node.js:
- Node.js 18.x LTS
- Node.js 20.x LTS
- Node.js 22.x LTS
When a Node.js version goes end of life, we will bump the major version of the Arcjet SDK. Technical support is provided for the current major version of the Arcjet SDK for all users and for the current and previous major versions for paid users. We will provide security fixes for the current and previous major SDK versions.
Next.js
Arcjet supports the current and previous major versions of Next.js for both the app router and pages router. When a new major version of Next.js is released, we will bump the major version of the Arcjet SDK.
- Previous supported major version: Next.js 14.x
- Current supported major version: Next.js 15.x
The pages router will be supported for as long as Next.js supports it.
Technical support is provided for the current major version of the Arcjet SDK for all users and for the current and previous major versions for paid users. We will provide security fixes for the current and previous major versions.