npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vpndetection-next

v2.1.19

Published

Official Next.js proxy/middleware for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN visitors on every request.

Readme

VPNDetection Next.js Middleware

npm license

The official Next.js proxy middleware for the VPNDetection API.

It classifies the visitor behind each request — VPN, residential proxy, Tor, hosting, CDN, relay — and forwards the answer to your app, where Server Components and Route Handlers can read it. Blocking is opt-in.

Getting Started

npm install vpndetection-next

Requires Node.js 22 or newer and Next.js 15 or newer. The proxy runs on the Node.js runtime and, on Next 15, on the Edge runtime too.

You need an API key on a paid plan. Create one in the console; without one a lookup is on Unlimited, the free plan, and leaves out the hosting, CDN and proxy flags, so a paid plan is what makes this usable in production rather than optional. A free account tries every field, 100 lookups a day.

Next 16 renamed the file convention from middleware.ts to proxy.ts, and the export from middleware to proxy. Both work on 16 — middleware.ts logs a deprecation — so use whichever your version supports.

// proxy.ts
import { createProxy } from 'vpndetection-next';

export const proxy = createProxy({ apiKey: process.env.VPNDETECTION_API_KEY });

export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };

On Next 15 the file is middleware.ts, and it runs on the Edge runtime unless its config asks for runtime: 'nodejs', which is stable from 15.5. Either works:

// middleware.ts, on Next 15
import { createProxy } from 'vpndetection-next';

export const middleware = createProxy({ apiKey: process.env.VPNDETECTION_API_KEY });

export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };

Then read the answer anywhere downstream:

// app/page.tsx
import { Suspense } from 'react';
import { headers } from 'next/headers';
import { readLookup } from 'vpndetection-next';

async function Greeting() {
    const lookup = readLookup(await headers());
    return lookup?.isVpn ? 'Hello, VPN user' : 'Hello';
}

export default function Page() {
    return (
        <main>
            <Suspense fallback="Hello">
                <Greeting />
            </Suspense>
        </main>
    );
}

The read of headers() sits inside <Suspense> because a new Next 16 app turns Cache Components on, and next build then refuses a page that reads request data outside one.

By default nothing is blocked. Your own code decides what the answer means — which is usually what you want, because whether a VPN visitor is a problem depends entirely on what they are doing.

Set a matcher. Without one the proxy runs on every request including static assets, which costs a lookup each.

Blocking

Pass a blockCondition and a matching request is answered with 403 and never reaches your app.

export const proxy = createProxy({
    apiKey: process.env.VPNDETECTION_API_KEY,
    blockCondition: { isVpn: true },
});

A condition is written in the shape of a result, and only the members you name are considered. That lets it reach the evidence, not just the flags:

blockCondition: { isVpn: true, vpn: { provider: 'nordvpn' } }         // one provider
blockCondition: { isResproxy: true, resproxy: { hits: { gte: 5 } } }  // a numeric threshold
blockCondition: { vpn: { confidence: ['high', 'medium'] } }           // any of these
blockCondition: [{ isTor: true }, { isResproxy: true }]               // a list is OR

Values are matched by equality, strings without regard to case. An array means any-of. { gte, gt, lte, lt } compares numbers, and every bound you give must hold, so two of them are a range. Members you set to false or null are ignored, so a condition states the signals you act on; one that constrains nothing would match every request, and is refused when the proxy is created rather than silently blocking all your traffic.

Replace the refusal with onBlocked — a redirect is usually friendlier than a 403:

onBlocked: (request) => NextResponse.redirect(new URL('/no-vpn', request.url)),

Doing other work in the same proxy

Most apps already have a proxy. Use createGuard and call it from yours:

import { NextResponse } from 'next/server';
import { createGuard } from 'vpndetection-next';

const guard = createGuard({ apiKey: process.env.VPNDETECTION_API_KEY });

export async function proxy(request: NextRequest) {
    const lookup = await guard(request);
    if (lookup?.result?.isTor) {
        return NextResponse.redirect(new URL('/verify', request.url));
    }
    return guard.forward(request, lookup);
}

guard.forward continues to your app carrying the answer. Return it rather than a bare NextResponse.next(), or nothing downstream can read the lookup.

Where the client address comes from

This is the setting that decides whether any of the above works, and Next makes it more your problem than most frameworks do.

Next has no socket peer to offer. NextRequest.ip was removed in Next 15, and a proxy runs ahead of the server, so a forwarded header is the only address available — this is the one place where the default is a header rather than a connection.

The default reads the left-most X-Forwarded-For entry. That is correct when the platform in front of you overwrites the header, which Vercel does and any ingress you control can be configured to. Behind something that appends, the left-most entry is whatever the visitor sent, and detection silently does nothing.

For a platform that writes its own header, name it:

import { headerIpSelector } from 'vpndetection-next';

createProxy({
    apiKey: process.env.VPNDETECTION_API_KEY,
    ipSelector: headerIpSelector('CF-Connecting-IP'),  // or True-Client-IP, or your own
});

If you know how many proxies sit in front, count from the right: xffIpSelector({ depth: 1 }) is the address your nearest proxy saw. Anything else, pass your own function — it receives the NextRequest and returns an address.

If the address resolves to a private one, or to nothing at all, the proxy says so once on console.warn. That is expected running locally and is the signal to fix your configuration anywhere else.

The forwarded header

The answer travels to your app on x-vpndetection, because a proxy runs in its own context and a request header is the only thing that crosses into it.

Any copy the caller sent is deleted before ours is written. That overwrite is the entire basis of trusting it downstream — without it a visitor could set the header themselves and tell your app whatever they liked about their own address. It is the same rule an ingress follows for a real-IP header, and it holds even when the request was skipped.

Turn it off with forward: false if you only ever block and never read the answer.

When a lookup fails

The request is let through, and the reason is on lookup.error. Our outage should not become yours, so a network failure, an exhausted quota or a rejected key all fail open. Pass failClosed: true to block instead.

Cost and latency

Answers are cached for an hour per proxy instance, so a returning visitor costs nothing, and private addresses never leave the process. A cache miss is one request to our API, bounded at 2500 ms by default and not retried — on a request path, failing open quickly beats holding a visitor while we try again.

createProxy({ apiKey: KEY, timeoutMs: 1000, retries: 1, cache: { max: 50000, ttlMs: 600000 } })

A serverless deployment gets less from the cache than a long-running one, because the instance holding it is short-lived. Narrow the matcher, or skip what you do not care about:

skip: (request) => request.nextUrl.pathname.startsWith('/api/health'),

Beyond a few million distinct visitors a day, stop calling the API per request: download the dataset and look addresses up locally instead.

Absent is not false

Unlimited, with a key or without one, leaves out isHosting, isCdn and the four proxy flags, and of the detail behind each flag it answers only Tor's and relay's. A field your plan does not include is absent, which means "not in your plan" rather than "checked, and no".

lookup.result?.is_hosting ?? false   // when you only want the flag

A blockCondition naming a member your plan does not serve can never match, so the proxy warns once instead of failing silently. Set onMissingField: 'throw' to make it an error.

Other Libraries

There are official VPNDetection client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/vpndetection-io for more.

About VPNDetection

VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.

License

This project is licensed under the MIT License.