Back to Blog

Next.js Middleware: Edge Auth Guards, A/B Testing, Geolocation Rewrites, and Request Inspection

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
December 8, 2026
13 min read

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 100+ 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];
}

๐Ÿš€ 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

See Also


Working With Viprasol

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 โ†’

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 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

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.