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

Code
npm install @agent-beam/beam
Code
// 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.

Code
// 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:

Code
// 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:

Code
beam.event("admin.granted", { user: target.id, detail: `by ${session.user.id}` });
beam.event("apikey.created", { user: session.user.id });

Protect LLM routes

Code
// 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

Code
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

Code
# .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:

RuleMeansSeverity
Middleware bypass attemptA 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 sourceMedium
Node.js package files served.npmrc, package-lock.json or node_modules answered 2xxHigh

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 productionBrowserSourceMaps off.
  • List only the hosts you need in images.remotePatterns.
  • Keep secrets out of NEXT_PUBLIC_ variables.

Coverage checklist

FeatureDone when
SDK in proxy.tsA test request shows on the dashboard
Stack declaredstack: "nextjs" in lib/beam.ts
Headers passedheaders: request.headers in beam.request()
Sign-in eventslogin.failed and login.succeeded from authorize
Admin and key eventsadmin.granted and apikey.created where they happen
LLM routesprompt(), output() and usage() on every model call, with a canary
Outbound hostsHosts your apps call lists your providers
Dependency checksThe CI job runs on pull requests
AlertsCritical pages someone, high reaches a channel (step 8)
RoutineSomeone 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
Explore the docs