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:
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:
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:
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:
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:
<script defer data-fl-website-id="flid_******" src="/js/main.js"></script>4. Deploy
The rewrite + middleware take effect on deploy.
Confirming It Works
- Open your site in a regular browser tab.
- Open DevTools → Network. Filter for
/api/track. - Confirm each request goes to your own domain (not
analytics.flowsery.com). - 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/trackif 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 } }), notresponse.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.).