Skip to main content

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.

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.
Recipe PathThis guide corresponds to recipes/vercel.

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

cd recipes/vercel
vercel env add ESPER_API_KEY
vercel
Operator ViewThis recipe is a good fit when you want a controlled starting point with very little platform work.

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