LogoRapidstack

Geolocation

Detect the visitor country with IPinfo or the Cloudflare CF-IPCountry header, then use it to default the display currency and localize content.

This template detects the visitor's country to pick the default display currency, and you can read it for your own features, such as localized content. It has two providers: IPinfo, which looks up the visitor's IP address, and Cloudflare, which reads the country from the CF-IPCountry header when your site runs behind the Cloudflare proxy.

Overview

  • Browser and server access - /api/ip returns the current visitor's location; server code calls getVisitorGeo directly.
  • Server-side lookups - The IPinfo token never reaches the browser.
  • Nothing stored - Geolocation does not save the visitor's location or IP.

Choose a provider

IPinfo (default)Cloudflare
Works whenAnywhereYour site runs behind the Cloudflare proxy
ReturnsCountry, plus more fields depending on your IPinfo planCountry only
CostOne IPinfo API request per lookupNone
NeedsAn IPinfo tokenCloudflare IP geolocation turned on

Pick Cloudflare when your site is already behind its proxy and you only need the country. Otherwise, use IPinfo.

Quick start

IPinfo

  1. Get a token from IPinfo.
  2. Add it to .env before you build, because the token is read at build time:
    .env
    NUXT_GEO_PROVIDER="ipinfo"
    IPINFO_TOKEN="your_ipinfo_token"
    
  3. Make sure your reverse proxy overwrites X-Forwarded-For. The visitor's IP comes from that header, so without the overwrite a visitor can send any IP they like.

Cloudflare

  1. Turn on Cloudflare IP geolocation for your site so proxied requests carry CF-IPCountry.
  2. Set the provider in .env locally and in your host's environment variables in production (see deployment). No token is needed:
    .env
    NUXT_GEO_PROVIDER="cloudflare"
    
  3. Configure your host or firewall to accept traffic from Cloudflare's IP ranges only. Anyone who can reach your origin directly can send their own CF-IPCountry header.

Read the location

From the browser

/api/ip returns the current visitor's location:

  • ipinfo: IPinfo's response for the visitor's IP, such as { ip, city, region, country, loc, org, postal, timezone }
  • cloudflare: { country }

Check for country rather than the status code. The endpoint answers 204 No Content with an empty body when the provider has no answer, and in ipinfo mode a private or loopback address (local development, or a proxy that does not forward the visitor IP) returns 200 without country.

From server code

server/api/my-country.get.ts
import { getVisitorGeo } from '@@/server/services/geo-server-service'

export default defineEventHandler(async event => {
  const geo = await getVisitorGeo(event)
  return { country: geo?.country ?? null }
})

getVisitorGeo uses the configured provider and never throws. It returns null when the provider has no answer, and in ipinfo mode a private address returns an object without country, so check country before using the result. In cloudflare mode it does not return the visitor IP; for rate limiting by IP, see rate limiting and client IP.

Good to know

  • NUXT_GEO_PROVIDER is read from the server's environment at startup, so switching providers takes a restart, not a rebuild. IPINFO_TOKEN is read at build time; to change it without rebuilding, set NUXT_IPINFO_TOKEN in the server's environment.
  • NUXT_GEO_PROVIDER is case-insensitive and ignores surrounding spaces. Unset or empty means ipinfo; any other value logs a warning at startup and falls back to ipinfo.
  • Cloudflare's no-country and Tor codes (XX, T1), and a missing or malformed header, count as an unknown country.
  • When an IPinfo lookup fails, for example because the token is missing or invalid, the server logs a warning and the endpoint answers 204.

Usage examples

These examples fetch the location in the browser after the page renders (server: false, lazy: true). A prerendered or cached page would otherwise serve the same location to every visitor.

Get the visitor's location

Which fields you get depends on the provider: cloudflare returns only country, and with ipinfo the fields depend on your plan. The example shows each field only when it is present.

<script setup lang="ts">
const { data: ipInfo } = useFetch('/api/ip', { server: false, lazy: true })
</script>

<template>
  <div v-if="ipInfo?.country">
    <p>Country: {{ ipInfo.country }}</p>
    <p v-if="ipInfo.city">City: {{ ipInfo.city }}</p>
    <p v-if="ipInfo.timezone">Timezone: {{ ipInfo.timezone }}</p>
  </div>
</template>

Default currency by country

The pricing tables pick the currency shown first from the visitor's country. usePricingCurrency gives your own components the same currency: it detects after mount, maps the country through the prices store, and falls back to paymentsConfig.defaultCurrency. Each component that calls it requests /api/ip when it mounts. Checkout charges the price the visitor selected; they can change its currency only through a currency selector, built in or your own (see User preference override).

<script setup lang="ts">
const { displayCurrency } = usePricingCurrency()
</script>

<template>
  <p>Prices in {{ displayCurrency.toUpperCase() }}</p>
</template>

Stripe checkout and currency

Charge the price the visitor saw. The pricing tables send the selected price ID and the checkout endpoint charges it, so the detected country only picks which currency is shown first. To let Stripe convert to the buyer's local currency, use Adaptive Pricing. To restrict who can buy, see Regional restrictions.

Additional use cases

Localized content

Show different content or messages based on the visitor's country:

<script setup lang="ts">
const { data: ipInfo } = useFetch('/api/ip', { server: false, lazy: true })

const welcomeMessage = computed(() => {
  const messages: Record<string, string> = {
    US: 'Welcome! 🇺🇸',
    DE: 'Willkommen! 🇩🇪',
    FR: 'Bienvenue! 🇫🇷',
  }
  return messages[ipInfo.value?.country ?? ''] ?? 'Welcome!'
})
</script>

<template>
  <p>{{ welcomeMessage }}</p>
</template>

Regional restrictions

The detected country is a display hint. A VPN changes it, and a visitor can spoof it unless your origin accepts traffic from Cloudflare alone (cloudflare) or your proxy overwrites X-Forwarded-For (ipinfo). Use it to pick a default currency or show a "not available in your region" notice, not to decide who can pay, what they pay, or what they can access.

To restrict purchases by country, use Stripe Radar rules, which Stripe evaluates on the payment.

User preference override

Let visitors pick their currency. PricingPlans and LandingPricingTable include a currency selector backed by the prices store; pass show-currency-selector to turn it on. To build your own selector, bind it to the same store so the pricing tables on the page show the visitor's choice:

<script setup lang="ts">
import { storeToRefs } from 'pinia'

const { selectedCurrency, availableCurrencies } = storeToRefs(usePricesStore())
</script>

<template>
  <Select v-model="selectedCurrency">
    <SelectTrigger>
      <SelectValue />
    </SelectTrigger>
    <SelectContent>
      <SelectItem v-for="currency in availableCurrencies" :key="currency" :value="currency">
        {{ currency.toUpperCase() }}
      </SelectItem>
    </SelectContent>
  </Select>
</template>

Privacy and performance

GDPR compliance: Geolocation processes the visitor's IP address. List geolocation and your provider (IPinfo or Cloudflare) in your privacy policy.

Caching: In ipinfo mode every lookup is an API request. Cache lookups if latency or your IPinfo quota matters.

Error handling: When the country is unknown in either mode, the pricing tables show paymentsConfig.defaultCurrency. With show-currency-selector on, the visitor can still pick any currency you price in.

Reference

Always provide fallback values (default country/currency) for an unknown location. It happens on network errors, a missing IPinfo token, or a request without CF-IPCountry.