How to Geo-Block: Restrict Website Access by Country
Geo-blocking restricts access to your website or application based on the visitor's geographic location. It's commonly used for licensing compliance, regulatory requirements, or security. Here's how to implement it at different layers of your stack.
Why Geo-Block?
- Licensing โ Streaming services restrict content by region due to distribution rights
- Compliance โ Financial services may be prohibited from serving certain jurisdictions
- Security โ Block regions with high fraud rates as a defense layer
- Sanctions โ Legal requirements to block access from sanctioned countries
Approach 1: Next.js Middleware (Edge)
Runs before the page loads, at the edge โ zero latency for blocked users:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const BLOCKED_COUNTRIES = new Set(["XX", "YY", "ZZ"]);
export function middleware(request: NextRequest) {
const country = request.headers.get("x-vercel-ip-country") || "";
if (BLOCKED_COUNTRIES.has(country)) {
return NextResponse.rewrite(
new URL("/blocked", request.url)
);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next|api|blocked|favicon.ico).*)"],
};Approach 2: Express Middleware
const BLOCKED = new Set(["XX", "YY", "ZZ"]);
app.use(async (req, res, next) => {
try {
const geo = await fetch("https://geo.kamero.ai/api/geo")
.then(r => r.json());
if (BLOCKED.has(geo.country)) {
return res.status(403).json({
error: "Access restricted in your region",
country: geo.country,
});
}
req.geo = geo;
next();
} catch {
next(); // Fail open โ don't block if API is unreachable
}
});Approach 3: Client-Side (Soft Block)
For a softer approach that shows a message instead of a hard block:
async function checkAccess() {
const { country } = await fetch(
"https://geo.kamero.ai/api/geo"
).then(r => r.json());
const blocked = ["XX", "YY", "ZZ"];
if (blocked.includes(country)) {
document.getElementById("app").innerHTML = `
<div class="blocked-message">
<h1>Service Unavailable</h1>
<p>This service is not available in your region.</p>
</div>
`;
return false;
}
return true;
}Approach 4: Allowlist (Opposite of Blocking)
Sometimes it's easier to define which countries CAN access your service:
const ALLOWED_COUNTRIES = new Set([
"US", "CA", "GB", "DE", "FR", "AU", "JP",
]);
function isAllowed(country: string): boolean {
return ALLOWED_COUNTRIES.has(country);
}Best Practices
- Fail open, not closed. If the geolocation API is unreachable, allow access rather than blocking everyone.
- Show a clear message. Don't just show a 403 โ explain why access is restricted and provide contact info.
- Log blocked attempts. Track how many users are being blocked for monitoring and compliance auditing.
- Consider VPN users. Geo-blocking is not foolproof โ VPN users can bypass it. For strict compliance, combine with other verification methods.
- Check legal requirements. Some jurisdictions require specific messaging or appeal processes when blocking access.
Creating a Blocked Page
// app/blocked/page.tsx
export default function BlockedPage() {
return (
<main style={{ textAlign: "center", padding: "4rem 2rem" }}>
<h1>Service Unavailable in Your Region</h1>
<p>
We're sorry, but this service is not currently available
in your location due to regulatory requirements.
</p>
<p>
If you believe this is an error, please contact{" "}
<a href="mailto:support@example.com">support@example.com</a>
</p>
</main>
);
}Get Visitor Country Instantly
Free API returns ISO country code with every request.
View Documentation โ