Next.js
Monitor a Next.js app with the in-process SDK: requests in proxy.ts, sign-ins through Auth.js, LLM routes checked, and Next.js-specific attacks like the middleware bypass caught.
On this page
Next.js apps use the SDK: detection runs inside the app, with no log files to collect. This recipe covers the App Router on Next.js 15 and 16, on Vercel or self-hosted.
What you'll have
- Every request checked in
proxy.ts, with the Next.js rules on and the middleware bypass (CVE-2025-29927) flagged as critical. - Sign-ins, admin grants and new API keys reported from your auth code, so a takeover after failed attempts is critical.
- Every LLM route checking prompts and output, reporting token use per user.
- New outbound hosts noticed.
- Dependencies checked on every pull request.
Before you start
Do steps 1 and 2 of the general cookbook first: App monitoring is on, the app is added, and its beam_app_ token is in your secret store as BEAM_APP_TOKEN. Everything below needs @agent-beam/beam 0.2.32 or newer.
Install and create one monitor
npm install @agent-beam/beam
// lib/beam.ts
import { createAppMonitor } from "@agent-beam/beam/monitor";
export const beam = createAppMonitor({
stack: "nextjs",
watchOutbound: true, // notes the host of each fetch(), never the URL or body
canaries: process.env.PROMPT_CANARY ? [process.env.PROMPT_CANARY] : [],
});
Add BEAM_APP_TOKEN (and a random PROMPT_CANARY) to the project's environment variables, for Production and Preview.
Check every request in proxy.ts
Next.js 16 calls this file proxy.ts with a proxy function; on 15 and earlier it's middleware.ts with middleware.
// proxy.ts
import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";
import { beam } from "@/lib/beam";
export function proxy(request: NextRequest, event: NextFetchEvent) {
beam.request({
method: request.method,
url: request.nextUrl.pathname + request.nextUrl.search,
ip: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
userAgent: request.headers.get("user-agent"),
headers: request.headers, // needed for the middleware-bypass rule, host and country
});
event.waitUntil(beam.flush());
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Pass headers: the middleware-bypass rule reads x-middleware-subrequest, which only Next.js itself should send. Proxy runs before the response, so findings here have no status. Beam still rolls up attacks on software Next.js doesn't run (WordPress, PHP, Laravel, Spring). If you proxy a WordPress blog under your domain, add proxiesOtherStacks: true.
Report sign-ins, admins and keys
With Auth.js (NextAuth v5) and a credentials provider:
// auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { headers } from "next/headers";
import { beam } from "@/lib/beam";
export const { handlers, auth, signIn } = NextAuth({
providers: [
Credentials({
async authorize(credentials) {
const ip = (await headers()).get("x-forwarded-for")?.split(",")[0]?.trim();
const user = await verifyPassword(credentials);
if (!user) { beam.event("login.failed", { user: await userIdFor(credentials), ip }); return null; }
beam.event("login.succeeded", { user: user.id, ip });
return user;
},
}),
],
});
Where roles or keys change, in a server action or route handler:
beam.event("admin.granted", { user: target.id, detail: `by ${session.user.id}` });
beam.event("apikey.created", { user: session.user.id });
Protect LLM routes
// app/api/chat/route.ts
import { after } from "next/server";
import { beam } from "@/lib/beam";
export async function POST(request: Request) {
const { message, userId } = await request.json();
const check = beam.prompt(message, { user: userId });
if (check.severity === "high" || check.severity === "critical") return Response.json({ error: "Refused" }, { status: 400 });
const result = await callModel(message); // your model call
beam.usage({ user: userId, tokens: result.usage.totalTokens, model: result.model });
after(() => beam.flush()); // send findings after the response, on serverless too
if (beam.output(result.text).flagged) return Response.json({ text: result.text, plain: true });
return Response.json({ text: result.text });
}
Put PROMPT_CANARY in your system prompt: if it ever appears in output, a critical finding says the prompt leaked. Pass retrieved documents and tool results through beam.prompt() too, with a label.
Prove it works
beam monitor status # app, level, stack, usage
beam monitor test # a test finding through your alert workflows
curl -s "https://your-app.example.com/?id=1%20UNION%20SELECT%201" -o /dev/null
Within a minute, the SQL injection test shows on App monitoring: under Needs action if the app answered 2xx, or counted under "needed nothing from you" if it refused it. Then sign in to a test account with a wrong password ten times and the right one once: Sign-in succeeded after 10 failed attempts appears as critical.
Check dependencies on every pull request
# .github/workflows/beam-cve.yml
name: Dependency check
on: [pull_request]
jobs:
cve:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx -y @agent-beam/beam cve check . --fail-on high
env:
BEAM_APP_TOKEN: ${{ secrets.BEAM_APP_TOKEN }}
Add response statuses (optional)
To know whether each attack was answered or refused, also stream your host's request logs into beam monitor --stdin with --stack nextjs (a Vercel log drain to a small service, or next start 2>&1 | beam monitor --stdin when self-hosting behind a proxy that logs status). Findings then show Answered or Refused instead of Outcome unknown.
What Beam watches for on this stack
Besides every built-in rule and the attack-in-progress signals:
| Rule | Means | Severity |
|---|---|---|
| Middleware bypass attempt | A client sent x-middleware-subrequest, which skips middleware on unpatched Next.js (CVE-2025-29927) | Critical |
| Source maps served publicly | /_next/static/*.js.map answered 2xx, exposing your source | Medium |
| Node.js package files served | .npmrc, package-lock.json or node_modules answered 2xx | High |
Attacks on other frameworks (WordPress probes, Laravel or Spring exploits, and so on) are rolled up as Not your stack when there's no status to say otherwise, and never alert.
Close what Beam would catch
Beam raises these when they're found; better that there's nothing to find:
- Run Next.js 15.2.3 or newer (14.2.25, 13.5.9 or 12.3.5 on older lines) for the middleware bypass fix, and don't rely on middleware alone for authorization: check the session in the route or server action too.
- Leave
productionBrowserSourceMapsoff. - List only the hosts you need in
images.remotePatterns. - Keep secrets out of
NEXT_PUBLIC_variables.
Coverage checklist
| Feature | Done when |
|---|---|
| SDK in proxy.ts | A test request shows on the dashboard |
| Stack declared | stack: "nextjs" in lib/beam.ts |
| Headers passed | headers: request.headers in beam.request() |
| Sign-in events | login.failed and login.succeeded from authorize |
| Admin and key events | admin.granted and apikey.created where they happen |
| LLM routes | prompt(), output() and usage() on every model call, with a canary |
| Outbound hosts | Hosts your apps call lists your providers |
| Dependency checks | The CI job runs on pull requests |
| Alerts | Critical pages someone, high reaches a channel (step 8) |
| Routine | Someone reads Needs action daily and Attack analytics weekly |
Set this up with your coding agent
Install the agentbeam-setup skill for Claude Code, Codex, Cursor, Gemini CLI, OpenCode and Copilot, then ask your agent to set up AgentBeam on this machine. Beam scans the skill before it installs.
npx -y @agent-beam/beam add skill agentbeam-setup -g --all-agents