Back to Blog

Next.js Middleware: Edge Auth Guards

Build production Next.js middleware: JWT auth guards at the edge, A/B testing with cookie assignment, geolocation-based content rewrites, bot detection, and rate limiting without hitting your origin server.

Viprasol Tech Team
13 min read
Updated 2026

Quick answer. Next.js middleware runs at the edge before page render or API execution, adding roughly 1-5ms instead of a database round-trip. It's the right place for JWT verification without a DB call, cookie-based sticky A/B assignment, geolocation routing, bot detection, and rate limiting, then redirect, rewrite, or pass through. Next.js middleware runs at the edge โ€” before your page renders, before your API route executes, before the request even reaches your origin server. That makes it the right place for auth checks, geolocation rewrites, A/B test assignment, bot filtering, and rate limiting: work that applies to every request and needs to be fast. A middleware auth check adds ~2ms at the edge instead of a round-trip to your database.

This post covers production middleware patterns: JWT verification without a DB call, sticky A/B test assignment via cookies, geolocation-based content routing, bot detection heuristics, and composing multiple middleware concerns cleanly.

How Middleware Works

Browser request โ†’ Cloudflare/Vercel Edge Network
                         โ”‚
                         โ–ผ
              middleware.ts (runs at edge, ~1โ€“5ms)
                    โ”œโ”€โ”€ redirect / rewrite / next()
                    โ””โ”€โ”€ modify request headers โ†’ origin
                                โ”‚
                                โ–ผ
                    App Router page / API route

Middleware runs before the cache โ€” so returned responses from middleware are not cached by default. Use next() to let the cache serve cached pages normally.


1. File and Matcher Configuration

// middleware.ts (must be at project root, not inside /app or /src/app)
import { NextRequest, NextResponse } from 'next/server';

export const config = {
  matcher: [
    // Match everything except static files and Next.js internals
    '/((?!_next/static|_next/image|favicon.ico|images/|fonts/).*)',
    // Or be explicit about what you DO match:
    // '/dashboard/:path*',
    // '/api/:path*',
    // '/(en|de|fr)/:path*',
  ],
};

export async function middleware(req: NextRequest): Promise<NextResponse> {
  // Compose multiple middleware concerns
  return compose(req, [
    authMiddleware,
    abTestMiddleware,
    geolocationMiddleware,
    botDetectionMiddleware,
  ]);
}

type MiddlewareFn = (req: NextRequest) => NextResponse | null;

function compose(req: NextRequest, fns: MiddlewareFn[]): NextResponse {
  for (const fn of fns) {
    const result = fn(req);
    if (result) return result; // Short-circuit on redirect/block
  }
  return NextResponse.next();
}

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 1000+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

2. JWT Auth Guard

// src/middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify, type JWTPayload } from 'jose';

// Public paths that don't require auth
const PUBLIC_PATHS = new Set([
  '/',
  '/login',
  '/register',
  '/forgot-password',
  '/blog',
  '/pricing',
  '/about',
]);

const PUBLIC_PREFIXES = ['/blog/', '/api/auth/', '/api/webhooks/'];

export function authMiddleware(req: NextRequest): NextResponse | null {
  const { pathname } = req.nextUrl;

  // Skip auth for public paths
  if (PUBLIC_PATHS.has(pathname)) return null;
  if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) return null;

  const token = req.cookies.get('access_token')?.value
    ?? req.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    // API routes: return 401
    if (pathname.startsWith('/api/')) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    // Pages: redirect to login
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Verify JWT at the edge (no DB call โ€” stateless)
  const payload = verifyTokenSync(token);
  if (!payload) {
    const loginUrl = new URL('/login', req.url);
    return NextResponse.redirect(loginUrl);
  }

  // Pass user identity to the origin via headers
  const response = NextResponse.next();
  response.headers.set('x-user-id', payload.sub ?? '');
  response.headers.set('x-tenant-id', payload.tenantId as string ?? '');
  response.headers.set('x-user-roles', (payload.roles as string[])?.join(',') ?? '');
  return response;
}

// Edge-compatible JWT verification (no Node.js crypto โ€” uses Web Crypto API)
function verifyTokenSync(token: string): JWTPayload | null {
  // Note: jwtVerify is async โ€” in real middleware, use a cached public key
  // and verify synchronously, or use a lightweight edge-compatible library
  try {
    // For HS256: decode and verify HMAC signature
    const [headerB64, payloadB64, sigB64] = token.split('.');
    if (!headerB64 || !payloadB64 || !sigB64) return null;

    const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));

    // Check expiry
    if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null;

    // In production: verify HMAC signature using Web Crypto API (async)
    // For now return decoded payload โ€” signature verified in API routes
    return payload as JWTPayload;
  } catch {
    return null;
  }
}

Async JWT Verification with jose

// For RS256 (asymmetric) โ€” fetch public key once and cache
import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL(`${process.env.NEXT_PUBLIC_API_URL}/api/auth/jwks`)
);

export async function verifyJWT(token: string): Promise<JWTPayload | null> {
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://viprasol.com',
      audience: 'viprasol-app',
    });
    return payload;
  } catch {
    return null;
  }
}

3. A/B Testing with Sticky Assignment

// src/middleware/ab-test.ts
import { NextRequest, NextResponse } from 'next/server';

interface ABTest {
  name: string;
  variants: string[];
  weight: number[];  // Must sum to 1.0
  paths: string[];   // Paths where this test applies
}

const ACTIVE_TESTS: ABTest[] = [
  {
    name: 'pricing-layout',
    variants: ['control', 'variant-a', 'variant-b'],
    weight: [0.5, 0.25, 0.25],
    paths: ['/pricing'],
  },
  {
    name: 'cta-copy',
    variants: ['control', 'variant-a'],
    weight: [0.5, 0.5],
    paths: ['/'],
  },
];

export function abTestMiddleware(req: NextRequest): NextResponse | null {
  const { pathname } = req.nextUrl;
  const response = NextResponse.next();

  for (const test of ACTIVE_TESTS) {
    if (!test.paths.some((p) => pathname === p || pathname.startsWith(p + '/'))) {
      continue;
    }

    const cookieName = `ab_${test.name}`;
    let variant = req.cookies.get(cookieName)?.value;

    // Assign variant if not already assigned
    if (!variant || !test.variants.includes(variant)) {
      variant = assignVariant(test.weight, test.variants);
      // Sticky: set cookie for 30 days
      response.cookies.set(cookieName, variant, {
        maxAge: 60 * 60 * 24 * 30,
        httpOnly: false, // Readable by client-side analytics
        sameSite: 'lax',
        secure: process.env.NODE_ENV === 'production',
      });
    }

    // Tell the origin which variant this user is in
    response.headers.set(`x-ab-${test.name}`, variant);

    // Rewrite to variant-specific page if it exists
    // e.g., /pricing โ†’ /_ab/pricing/variant-a (internal route)
    if (variant !== 'control') {
      const variantUrl = req.nextUrl.clone();
      variantUrl.pathname = `/_ab${pathname}/${variant}`;
      // Only rewrite if variant page exists โ€” check via headers or static manifest
    }
  }

  return null; // Let compose() call NextResponse.next() with modified headers
}

function assignVariant(weights: number[], variants: string[]): string {
  const rand = Math.random();
  let cumulative = 0;
  for (let i = 0; i < weights.length; i++) {
    cumulative += weights[i];
    if (rand < cumulative) return variants[i];
  }
  return variants[variants.length - 1];
}

next.js - Next.js Middleware: Edge Auth Guards

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

4. Geolocation-Based Routing

// src/middleware/geolocation.ts
import { NextRequest, NextResponse } from 'next/server';

// Vercel provides geo data; on other platforms use Cloudflare headers
function getCountry(req: NextRequest): string | undefined {
  return (
    req.headers.get('x-vercel-ip-country') ??   // Vercel
    req.headers.get('cf-ipcountry') ??           // Cloudflare
    req.geo?.country                             // Vercel geo object
  );
}

const EU_COUNTRIES = new Set([
  'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'SE', 'NO', 'DK',
  'FI', 'PT', 'IE', 'PL', 'CZ', 'HU', 'RO', 'GR', 'BG', 'HR',
]);

export function geolocationMiddleware(req: NextRequest): NextResponse | null {
  const { pathname } = req.nextUrl;
  const country = getCountry(req);

  if (!country) return null;

  // GDPR cookie banner: add header for EU users
  if (EU_COUNTRIES.has(country)) {
    const response = NextResponse.next();
    response.headers.set('x-gdpr-required', 'true');
    return response;
  }

  // Geo-blocked content
  if (pathname.startsWith('/features/crypto-trading')) {
    const BLOCKED_COUNTRIES = new Set(['CN', 'IR', 'KP', 'RU']);
    if (BLOCKED_COUNTRIES.has(country)) {
      return NextResponse.rewrite(new URL('/unavailable-in-region', req.url));
    }
  }

  // Locale detection: redirect to localized path
  if (pathname === '/' && !req.cookies.has('locale-set')) {
    if (country === 'DE') {
      return NextResponse.redirect(new URL('/de', req.url));
    }
    if (country === 'FR') {
      return NextResponse.redirect(new URL('/fr', req.url));
    }
  }

  return null;
}

5. Bot Detection

// src/middleware/bot-detection.ts
import { NextRequest, NextResponse } from 'next/server';

const BOT_UA_PATTERNS = [
  /googlebot/i, /bingbot/i, /slurp/i, /duckduckbot/i,
  /baiduspider/i, /yandexbot/i, /sogou/i, /exabot/i,
  /facebot/i, /ia_archiver/i,
];

const GOOD_BOTS = new Set([
  'googlebot', 'bingbot', 'slurp', 'duckduckbot', // Allow search crawlers
]);

export function botDetectionMiddleware(req: NextRequest): NextResponse | null {
  const ua = req.headers.get('user-agent') ?? '';

  const isBot = BOT_UA_PATTERNS.some((p) => p.test(ua));
  if (!isBot) return null;

  const botName = BOT_UA_PATTERNS.find((p) => p.test(ua))
    ?.toString()
    .replace(/[/\\^$*+?.()|[\]{}]/g, '');

  // Block bad bots entirely
  if (!GOOD_BOTS.has(botName?.toLowerCase() ?? '')) {
    return new NextResponse(null, { status: 403 });
  }

  // For good bots: skip A/B tests and personalization
  const response = NextResponse.next();
  response.headers.set('x-is-bot', 'true');
  return response;
}

6. Edge Rate Limiting

// src/middleware/rate-limit.ts
// Edge rate limiting using Upstash Redis (edge-compatible HTTP API)
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 req/min
  analytics: true,
  prefix: 'rl',
});

export async function rateLimitMiddleware(req: NextRequest): Promise<NextResponse | null> {
  // Only rate-limit API routes
  if (!req.nextUrl.pathname.startsWith('/api/')) return null;

  const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
    ?? req.headers.get('x-real-ip')
    ?? '127.0.0.1';

  const { success, remaining, reset } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      {
        status: 429,
        headers: {
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': reset.toString(),
          'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
        },
      }
    );
  }

  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Remaining', remaining.toString());
  return response;
}

Performance Characteristics

Middleware taskEdge latencyOrigin latencyRecommendation
JWT decode (no verify)< 1msโ€”โœ… Edge
JWT verify (HMAC/RSA)1โ€“3msโ€”โœ… Edge
Cookie read/write< 1msโ€”โœ… Edge
Geolocation header read< 1msโ€”โœ… Edge
Rate limit (Upstash)5โ€“15msโ€”โœ… Edge
Database session lookupN/A20โ€“100msโŒ Origin only
File system accessNot availableโ€”โŒ Not in middleware

Explore More


What We Bring to the Table

Need auth guards, A/B testing, or geolocation routing that runs at the edge without adding latency to every request? We implement Next.js middleware pipelines that compose multiple concerns cleanly โ€” JWT verification, A/B test assignment, bot filtering, and edge rate limiting โ€” with no database calls in the request path.

Talk to our team โ†’ | See our web development services โ†’

next.jsmiddlewareedgetypescriptperformanceautha/b-testing
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.