Kamero

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?

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

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 โ†’