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

Source: https://docs.agentbeam.com/cookbooks/frameworks/nextjs

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](https://docs.agentbeam.com/cookbooks/app-monitoring#step-1-turn-it-on-and-create-the-apps-token) 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

```sh
npm install @agent-beam/beam
```

```ts
// 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`.

```ts
// 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:

```ts
// 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:

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

## Protect LLM routes

```ts
// 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

```sh
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

```yaml
# .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](https://docs.agentbeam.com/agent-cli/app-monitoring#levels-and-detection) and the [attack-in-progress signals](https://docs.agentbeam.com/agent-cli/app-monitoring#signs-an-attack-is-underway):

| 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 `productionBrowserSourceMaps` off.
- 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](https://docs.agentbeam.com/cookbooks/app-monitoring#step-8-send-alerts-to-the-right-place)) |
| Routine | Someone reads **Needs action** daily and **Attack analytics** weekly |
