next-geo-guard
v1.0.0
Published
Protect Next.js apps from traffic originating outside allowed countries. Multi-source geo verification (Vercel headers, ISP lookup, optional auth metadata).
Downloads
105
Maintainers
Readme
next-geo-guard
Protect your Next.js app from traffic originating outside allowed countries. Uses multi-source geolocation verification (2-of-3) to reduce false positives from VPNs and bad IP data.
Features
- Multi-source verification: Vercel headers + ISP lookup + optional user metadata (e.g. Clerk)
- Configurable: Allow multiple countries (US, CA, etc.), customize required passes
- No lock-in: Works with or without auth; user metadata is optional
- ISP providers: ip-api.com (free) or ipinfo.io (with token for higher limits)
- Edge-ready: Runs in Next.js middleware
Install
npm install next-geo-guardQuick Start
1. Add to your middleware
// middleware.ts
import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server';
import { createGeoGuard } from 'next-geo-guard';
const geoGuard = createGeoGuard({
allowedCountries: ['US'],
blockedPath: '/blocked',
bypassPaths: ['/api/geo/debug', '/sign-in', '/sign-up', '/'],
});
export default clerkMiddleware(async (auth, req) => {
const geoResponse = await geoGuard(req, async () => {
const { userId } = await auth();
if (!userId) return null;
const user = await clerkClient().users.getUser(userId);
return { ...user.publicMetadata, ...user.privateMetadata } as Record<string, unknown>;
});
if (geoResponse) return geoResponse;
// ... rest of your middleware
});2. Without auth (Vercel + ISP only)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createGeoGuard } from 'next-geo-guard';
const geoGuard = createGeoGuard({
allowedCountries: ['US', 'CA'],
blockedPath: '/blocked',
});
export async function middleware(req: NextRequest) {
const geoResponse = await geoGuard(req);
if (geoResponse) return geoResponse;
return NextResponse.next();
}3. Create a blocked page
Add /blocked (or your blockedPath) to show when users are restricted. Query params include country, passes, total, reason.
Configuration
| Option | Default | Description |
|--------|---------|-------------|
| allowedCountries | ['US'] | Country codes that pass verification |
| requiredPasses | 2 | When 2+ sources available, require this many to pass |
| blockedPath | '/blocked' | Path to rewrite/redirect to when blocked |
| bypassPaths | [] | Paths that skip geo check (e.g. sign-in, debug) |
| useRewrite | true | Use NextResponse.rewrite vs redirect |
| verifiedMaxAgeMs | 24h | How long to trust user metadata before re-verifying |
| userMetadataKeys | { country: 'verifiedCountry', verifiedAt: 'verifiedCountryAt' } | Keys to read from user metadata |
Environment Variables
| Variable | Description |
|----------|-------------|
| IPINFO_ACCESS_TOKEN | Optional. Use ipinfo.io instead of ip-api.com (higher limits, HTTPS) |
| GEO_XFF_USE_LAST | Set to 'true' to use last IP from x-forwarded-for (for some proxy setups) |
| GEO_GUARD_ALWAYS_RUN | Set to 'true' to run geo checks in development |
| NODE_ENV | In production, geo checks run. In development, they are skipped unless GEO_GUARD_ALWAYS_RUN=true |
API Routes (Optional)
Debug endpoint
Help users diagnose VPN/geo issues:
// app/api/geo/debug/route.ts
import { NextResponse } from 'next/server';
import { getClientIp } from 'next-geo-guard';
export const dynamic = 'force-dynamic';
export const runtime = 'edge';
export async function GET(request: Request) {
const headers = request.headers;
const clientIp = getClientIp(headers);
const geoHeaders: Record<string, string> = {};
for (const key of ['x-vercel-ip-country', 'x-vercel-forwarded-for', 'x-forwarded-for']) {
const val = headers.get(key);
if (val) geoHeaders[key] = val;
}
return NextResponse.json({ clientIp: clientIp ?? 'NOT FOUND', geoHeaders });
}Status endpoint
// app/api/geo/status/route.ts
import { NextResponse } from 'next/server';
import { verifyGeoAccess } from 'next-geo-guard';
export async function GET(request: Request) {
const user = null; // or get from your auth (Clerk, etc)
const geoResult = await verifyGeoAccess(request.headers, user);
return NextResponse.json({
allowed: geoResult.allowed,
passes: geoResult.passes,
total: geoResult.total,
details: geoResult.details,
});
}Clerk integration
The package expects user metadata with verifiedCountry and verifiedCountryAt (configurable via userMetadataKeys). When a user passes verification, store:
await clerkClient().users.updateUserMetadata(userId, {
publicMetadata: {
verifiedCountry: 'US',
verifiedCountryAt: Date.now(),
},
});The package trusts this for 24h (verifiedMaxAgeMs), then requires re-verification.
How it works
- Vercel – reads
x-vercel-ip-countryfrom request headers (available on Vercel deployments) - ISP – looks up client IP via ip-api.com or ipinfo.io
- User – optional 3rd source from auth metadata (set when user previously passed)
Access is allowed when at least 2 of 3 sources agree on an allowed country. If Vercel and ISP contradict (e.g. behind a proxy), access is blocked for safety.
Publishing
To publish to npm:
cd packages/next-geo-guard
npm run build
npm publishOr use a scoped package: "name": "@yourorg/next-geo-guard" in package.json.
License
MIT
