Support/Proxy Guides
Proxy Guides

Proxying Flowsery Analytics with Next.js

Route Flowsery Analytics through your own domain in a Next.js application to prevent adblocker interference and capture accurate visitor data.

There are two steps to a full proxy: a rewrite (makes the tracker first-party so it survives adblockers) and middleware (forwards the real visitor IP to Flowsery's backend). Rewrites alone will deliver events but every visitor will appear from your server's region — you need both.

1. Configure Rewrites

Add or update your next.config.js at the project root:

JavaScript
module.exports = {
  async rewrites() {
    return [
      {
        source: '/js/main.js',
        destination: 'https://cdn.flowsery.com/main.js',
      },
      {
        source: '/api/track',
        destination: 'https://analytics.flowsery.com/analytics/events',
      },
      {
        source: '/:locale/api/track',
        destination: 'https://analytics.flowsery.com/analytics/events',
      },
    ];
  },
};

If your project already uses /api/track, pick another path and pass it via data-api on the tracker script (e.g. data-api="/flowsery-events").

2. Inject the Real Visitor IP (middleware)

Next.js rewrites do a server-to-server fetch, so the downstream Cloudflare edge replaces cf-connecting-ip with your Next.js server's IP. Flowsery then sees every visitor as "your server" and resolves geo to your hosting region.

To fix this, add middleware that reads the real visitor IP on arrival and pins it to a custom header x-flowsery-real-ip. This custom header survives the proxy hop untouched, and Flowsery's backend prefers it over cf-connecting-ip when present.

Create or update middleware.ts at the project root:

TypeScript
import { NextRequest, NextResponse } from 'next/server';
 
const TRACKING_PROXY_PATHS = /\/api\/track$/;
 
export default function middleware(request: NextRequest) {
  if (!TRACKING_PROXY_PATHS.test(request.nextUrl.pathname)) {
    return NextResponse.next();
  }
 
  const realIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
 
  if (!realIp) return NextResponse.next();
 
  const forwardedHeaders = new Headers(request.headers);
  forwardedHeaders.set('x-flowsery-real-ip', realIp);
 
  return NextResponse.next({
    request: { headers: forwardedHeaders },
  });
}
 
export const config = {
  matcher: ['/api/track', '/:locale/api/track'],
};

If you already have middleware

Two things to merge carefully:

1. The handler. Call the tracking-proxy logic first for /api/track paths, then fall through to your existing logic:

TypeScript
export default function middleware(request: NextRequest) {
  // Runs for /api/track + /:locale/api/track. Returns a response only when
  // the path matches, otherwise returns NextResponse.next() so your existing
  // logic runs below.
  if (/\/api\/track$/.test(request.nextUrl.pathname)) {
    const realIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
    if (realIp) {
      const headers = new Headers(request.headers);
      headers.set('x-flowsery-real-ip', realIp);
      return NextResponse.next({ request: { headers } });
    }
    return NextResponse.next();
  }
 
  // ...your existing middleware logic here (locale detection, auth, etc.)
}

2. The matcher. Most page-level middleware uses a negative lookahead like /((?!api/|_next/...).*) to exclude API routes. That pattern will also exclude /api/track, so middleware never fires for the tracking path. Extend the matcher array to re-include it:

TypeScript
export const config = {
  matcher: [
    // Your existing page-level pattern — leave it alone.
    '/((?!api/|js/|_next/static|_next/image|favicon.ico|locales|ingest).*)',
 
    // Added so middleware also runs for the tracking proxy paths.
    '/api/track',
    '/:locale/api/track',
  ],
};

The order doesn't matter. The matcher entries are OR'd — a path triggers middleware if any entry matches it. Page routes keep hitting your existing pattern; /api/track hits the new explicit entry and reaches your tracking-proxy handler above.

3. Modify the Script Tag

Point the script at the proxied local path:

HTML
<script defer data-fl-website-id="flid_******" src="/js/main.js"></script>

4. Deploy

The rewrite + middleware take effect on deploy.

Confirming It Works

  1. Open your site in a regular browser tab.
  2. Open DevTools → Network. Filter for /api/track.
  3. Confirm each request goes to your own domain (not analytics.flowsery.com).
  4. In your Flowsery dashboard, check that visitor countries and cities reflect your audience (not your hosting region).

Troubleshooting

Every visitor appears from the same location

If all visitors show a single geographic location (typically your server's region), the middleware step isn't applying the x-flowsery-real-ip header.

Common causes:

  • Middleware matcher doesn't include the tracking path. Most existing matchers use a negative lookahead like /((?!api/|...).*) which excludes /api/*. Add /api/track (and /:locale/api/track if you use locale-prefixed URLs) as explicit entries to the matcher array.
  • You added the header to the response instead of the request. The backend reads request headers; use NextResponse.next({ request: { headers } }), not response.headers.set(...).
  • Your edge provider isn't Cloudflare. If you sit behind Vercel, Fly.io, or another edge, read the visitor IP from that edge's header instead (x-real-ip, x-vercel-forwarded-for, fly-client-ip, etc.).