> ## Documentation Index
> Fetch the complete documentation index at: https://docs.esperr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Vercel

> Run Esper in front of a Vercel-served application — use this recipe when your app already runs on Vercel and you want Esper checks at the serverless boundary.

<Info>
  **What you'll need**

  1. A deployed Esper API that your Vercel deployment can reach.
  2. An Esper tenant for the Vercel application you want to protect.
  3. An Esper API key for that tenant, stored as a Vercel environment variable.
  4. At least one mitigation and policy in Esper.
  5. If you want managed challenge redirects, a challenge mitigation with a
     default return URL template already configured.
  6. Middleware or edge-route coverage for the paths you want Esper to evaluate.
</Info>

<Info>
  **Recipe Path**

  This guide corresponds to
  <a href="https://github.com/stonehedgelabs/esper/tree/main/recipes/vercel" target="_blank" rel="noopener noreferrer">`recipes/vercel`</a>.
</Info>

## What this integration does

The Vercel middleware accepts the incoming request, asks Esper for one runtime
decision, and then either:

* allows the request to continue
* redirects the request into an Esper-managed challenge
* returns a block response immediately

Minimal middleware flow:

```ts theme={null}
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const url = request.nextUrl;

  const runtimePayload = {
    source: "vercel-edge",
    observed_at: new Date().toISOString(),
    idempotency_key: crypto.randomUUID(),
    return_url: request.url,
    request: {
      method: request.method,
      path: url.pathname,
      headers: {
        "user-agent": request.headers.get("user-agent") || "",
        "x-forwarded-for": request.headers.get("x-forwarded-for") || "",
        "x-ipua": request.headers.get("x-ipua") || "",
      },
      query_params: Object.fromEntries(url.searchParams.entries()),
      cookies: Object.fromEntries(
        request.cookies.getAll().map((cookie) => [cookie.name, cookie.value]),
      ),
      route_params: {},
    },
    // Esper runtime ingest rejects empty fields, so include at least one field.
    fields: {
      probe: "1",
    },
  };

  const esperResponse = await fetch("https://api.esperr.com/api/v1/runtime/mitigation", {
    method: "POST",
    headers: {
      "x-esper-api-key": process.env.ESPER_API_KEY!,
      "content-type": "application/json",
    },
    body: JSON.stringify(runtimePayload),
  });

  if (!esperResponse.ok) {
    return new NextResponse("Esper runtime failure", { status: 502 });
  }

  const decision = await esperResponse.json();

  switch (decision.action?.type) {
    case "allow":
      return NextResponse.next();
    case "challenge":
      return NextResponse.redirect(decision.action.challenge.redirect_url);
    case "block": {
      const block = decision.action.block;
      return new NextResponse(block?.body || "Blocked", {
        status: block?.status_code || 403,
        headers: block?.headers || {},
      });
    }
    default:
      return new NextResponse("Invalid Esper decision", { status: 502 });
  }
}

export const config = {
  matcher: ["/login/:path*", "/signup/:path*", "/api/:path*"],
};
```

## Environment variables

* `ESPER_API_KEY`

## Deploy

```bash theme={null}
cd recipes/vercel
vercel env add ESPER_API_KEY
vercel
```

<Tip>
  **Operator View**

  This recipe is a good fit when you want a controlled starting point with very
  little platform work.
</Tip>

## Current runtime behavior

* `allow`: continue to your normal Vercel app route or API handler.
* `challenge`: redirect immediately to Esper's returned `redirect_url`.
* `block`: return the deny response immediately.

## Challenge proofs

Esper-managed challenges return visitors to the protected URL with an
`esper_challenge_proof` query parameter and can also set the configured proof
cookie. Forward request query parameters and cookies in the
`/runtime/mitigation` payload, as shown above. Esper Cloud validates a matching
proof before policy evaluation and returns `allow` without writing another
challenge decision or challenge session.

## Challenge redirects

Send `return_url: request.url` with each runtime mitigation request. This is the
original URL the visitor requested, and Esper stores it on the challenge session
so a successful challenge can return the visitor to the protected page. The
mitigation's default return URL template is only a fallback when the integration
does not provide a per-request `return_url`.
