diff --git a/examples/nextjs-14-app-dir-validate-email/.eslintrc.json b/examples/nextjs-14-app-dir-validate-email/.eslintrc.json
new file mode 100644
index 000000000..bffb357a7
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/nextjs-14-app-dir-validate-email/.gitignore b/examples/nextjs-14-app-dir-validate-email/.gitignore
new file mode 100644
index 000000000..fd3dbb571
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/nextjs-14-app-dir-validate-email/README.md b/examples/nextjs-14-app-dir-validate-email/README.md
new file mode 100644
index 000000000..76483ae7d
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/README.md
@@ -0,0 +1,30 @@
+
+
+
+
+# Arcjet email verification with Next.js 14 using the App Router
+
+This example shows how to use Arcjet with a Next.js [route
+handler](https://nextjs.org/docs/app/building-your-application/routing/route-handlers).
+
+## How to use
+
+1. From the root of the project, install the dependencies.
+
+ ```bash
+ npm ci
+ ```
+
+2. Enter this directory and start the dev server.
+
+ ```bash
+ cd examples/nextjs-14-app-dir-validate-email
+ npm run dev
+ ```
+
+3. Visit `http://localhost:3000/api/arcjet`.
+4. Refresh the page to see the email verification fail due to no MX records
+ existing on the domain.
diff --git a/examples/nextjs-14-app-dir-validate-email/app/api/arcjet/route.ts b/examples/nextjs-14-app-dir-validate-email/app/api/arcjet/route.ts
new file mode 100644
index 000000000..05f95d49d
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/app/api/arcjet/route.ts
@@ -0,0 +1,33 @@
+import arcjet, { validateEmail } from "@arcjet/next";
+import { NextResponse } from "next/server";
+
+const aj = arcjet({
+ key: "ajkey_yourkey",
+ rules: [
+ validateEmail({
+ mode: "LIVE",
+ block: ["NO_MX_RECORDS"],
+ }),
+ ],
+});
+
+export async function GET(req: Request) {
+ const decision = await aj.protect(req, {
+ email: "test@arcjet.co",
+ });
+
+ if (decision.isDenied()) {
+ return NextResponse.json(
+ {
+ error: "Forbidden",
+ },
+ {
+ status: 403,
+ },
+ );
+ }
+
+ return NextResponse.json({
+ message: "Hello world",
+ });
+}
diff --git a/examples/nextjs-14-app-dir-validate-email/app/favicon.ico b/examples/nextjs-14-app-dir-validate-email/app/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/examples/nextjs-14-app-dir-validate-email/app/favicon.ico differ
diff --git a/examples/nextjs-14-app-dir-validate-email/app/globals.css b/examples/nextjs-14-app-dir-validate-email/app/globals.css
new file mode 100644
index 000000000..fd81e8858
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/app/globals.css
@@ -0,0 +1,27 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ rgb(var(--background-end-rgb))
+ )
+ rgb(var(--background-start-rgb));
+}
diff --git a/examples/nextjs-14-app-dir-validate-email/app/layout.tsx b/examples/nextjs-14-app-dir-validate-email/app/layout.tsx
new file mode 100644
index 000000000..323bd9c95
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/examples/nextjs-14-app-dir-validate-email/app/page.tsx b/examples/nextjs-14-app-dir-validate-email/app/page.tsx
new file mode 100644
index 000000000..f870fe109
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/app/page.tsx
@@ -0,0 +1,113 @@
+import Image from "next/image";
+
+export default function Home() {
+ return (
+
+
+
+ Get started by editing
+ app/page.tsx
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/nextjs-14-app-dir-validate-email/next.config.js b/examples/nextjs-14-app-dir-validate-email/next.config.js
new file mode 100644
index 000000000..658404ac6
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+module.exports = nextConfig;
diff --git a/examples/nextjs-14-app-dir-validate-email/package.json b/examples/nextjs-14-app-dir-validate-email/package.json
new file mode 100644
index 000000000..8ef213f01
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "nextjs-14-app-dir-validate-email",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@arcjet/next": "*",
+ "react": "^18",
+ "react-dom": "^18",
+ "next": "14.0.4"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0",
+ "eslint": "^8",
+ "eslint-config-next": "14.0.4"
+ }
+}
diff --git a/examples/nextjs-14-app-dir-validate-email/postcss.config.js b/examples/nextjs-14-app-dir-validate-email/postcss.config.js
new file mode 100644
index 000000000..12a703d90
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/examples/nextjs-14-app-dir-validate-email/public/next.svg b/examples/nextjs-14-app-dir-validate-email/public/next.svg
new file mode 100644
index 000000000..db773047f
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/nextjs-14-app-dir-validate-email/public/vercel.svg b/examples/nextjs-14-app-dir-validate-email/public/vercel.svg
new file mode 100644
index 000000000..4bc795bda
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/nextjs-14-app-dir-validate-email/tailwind.config.ts b/examples/nextjs-14-app-dir-validate-email/tailwind.config.ts
new file mode 100644
index 000000000..7e4bd91a0
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/tailwind.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ backgroundImage: {
+ "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
+ "gradient-conic":
+ "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ },
+ },
+ },
+ plugins: [],
+};
+export default config;
diff --git a/examples/nextjs-14-app-dir-validate-email/tsconfig.json b/examples/nextjs-14-app-dir-validate-email/tsconfig.json
new file mode 100644
index 000000000..c71469637
--- /dev/null
+++ b/examples/nextjs-14-app-dir-validate-email/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/package-lock.json b/package-lock.json
index 4cb242671..b4cbe4253 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -352,6 +352,35 @@
}
}
},
+ "examples/nextjs-14-app-dir-validate-email": {
+ "version": "0.1.0",
+ "dependencies": {
+ "@arcjet/next": "*",
+ "next": "14.0.4",
+ "react": "^18",
+ "react-dom": "^18"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "14.0.4",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0",
+ "typescript": "^5"
+ }
+ },
+ "examples/nextjs-14-app-dir-validate-email/node_modules/@types/node": {
+ "version": "20.10.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
+ "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
"examples/nextjs-14-pages-wrap": {
"version": "0.1.0",
"dependencies": {
@@ -6315,6 +6344,10 @@
"resolved": "examples/nextjs-14-app-dir-rl",
"link": true
},
+ "node_modules/nextjs-14-app-dir-validate-email": {
+ "resolved": "examples/nextjs-14-app-dir-validate-email",
+ "link": true
+ },
"node_modules/nextjs-14-pages-wrap": {
"resolved": "examples/nextjs-14-pages-wrap",
"link": true