Skip to content

Arcjet / Clerk integration

Arcjet and Clerk work well together to provide rate limiting based on user authentication. Clerk provides the user authentication and Arcjet provides the rate limiting.

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.

Example use case

  • Limiting access to a free API endpoint based on the client IP address.
  • Providing a higher rate limit for authenticated clients based on their Clerk user ID.

See an example Next.js implementation on GitHub.

Rate limits using Clerk user ID

Arcjet rate limits allow custom characteristics to identify the client and apply the limit. Using Clerk’s currentUser() (app router) or getAuth() (pages router) helpers you can pass through a user ID.

/app/api/private/route.ts
import arcjet, { tokenBucket } from "@arcjet/next";
import { currentUser } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
// The arcjet instance is created outside of the handler
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
characteristics: ["userId"], // Track based on the Clerk userId
rules: [
// Create a token bucket rate limit. Other algorithms are supported.
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
}),
],
});
export async function GET(req: Request) {
// Get the current user from Clerk
const user = await currentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Deduct 5 tokens from the user's bucket
const decision = await aj.protect(req, { userId: user.id, requested: 5 });
if (decision.isDenied()) {
return NextResponse.json(
{
error: "Too Many Requests",
reason: decision.reason,
},
{
status: 429,
},
);
}
return NextResponse.json({ message: "Hello World" });
}
/middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
const isProtectedRoute = createRouteMatcher(["/api/private"]);
export default clerkMiddleware((auth, request) => {
if (isProtectedRoute(request)) {
auth().protect();
}
return NextResponse.next();
});
export const config = {
// Protects all routes, including api/trpc.
// See https://clerk.com/docs/references/nextjs/clerk-middleware
// for more information about configuring your Middleware
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

Chaining middleware

If you want to protect every page with Arcjet Shield automatically you can run it through Next.js middleware. Clerk also uses middleware to add authentication to your pages. You can chain the two together.

See an example Next.js implementation on GitHub.

/middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import arcjet, { createMiddleware, shield } from "@arcjet/next";
import { NextResponse } from "next/server";
export const config = {
// Protects all routes, including api/trpc.
// See https://clerk.com/docs/references/nextjs/clerk-middleware
// for more information about configuring your Middleware
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
const isProtectedRoute = createRouteMatcher(["/api/private"]);
const clerk = clerkMiddleware((auth, request) => {
if (isProtectedRoute(request)) {
auth().protect();
}
return NextResponse.next();
});
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
rules: [
// Protect against common attacks with Arcjet Shield
shield({
mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
}),
],
});
// Clerk middleware will run after the Arcjet middleware. You could also use
// Clerk's beforeAuth options to run Arcjet first. See
// https://clerk.com/docs/references/nextjs/auth-middleware#use-before-auth-to-execute-middleware-before-authentication
export default createMiddleware(aj, clerk);

Discussion