Geolocation
This template includes IP geolocation powered by IPinfo, enabling you to detect user location for features like dynamic pricing, localized content, and country-specific experiences.
Overview
The geolocation integration provides:
- IP-based location detection - Country, city, region, timezone.
- Two API endpoints - Get current user's location or query specific IPs.
- Server-side processing - Secure API key management.
- Privacy-focused - No personal data storage required.
Configuration
Add your IPinfo API token to .env:
IPINFO_TOKEN="your_ipinfo_token"
API endpoints
The template includes two endpoints:
/api/ip - Returns IP info for the current request
Returns: { ip, hostname, city, region, country, loc, org, postal, timezone }
/api/ip/[ipAddress] - Returns IP info for a specific IP address
Example: /api/ip/8.8.8.8
Both endpoints use getIpInfo() from server/services/ip-server-service.ts.
Usage examples
Get current user's location
<script setup>
const { data: ipInfo } = await useFetch('/api/ip')
</script>
<template>
<div>
<p>Country: {{ ipInfo?.country }}</p>
<p>City: {{ ipInfo?.city }}</p>
<p>Timezone: {{ ipInfo?.timezone }}</p>
</div>
</template>
Dynamic pricing by country
Detect user's country and show appropriate currency:
<script setup>
const { data: ipInfo } = await useFetch('/api/ip')
const currency = computed(() => {
const country = ipInfo.value?.country
if (country === 'GB') return 'GBP'
if (['DE', 'FR', 'ES', 'IT'].includes(country)) return 'EUR'
return 'USD'
})
const price = computed(() => {
const prices = { USD: 29, EUR: 25, GBP: 22 }
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.value,
}).format(prices[currency.value])
})
</script>
<template>
<div>{{ price }}/month</div>
</template>
Stripe checkout with location-based pricing
Pass the user's country to your checkout endpoint:
export default defineEventHandler(async event => {
const { plan, interval } = await readBody(event)
// Get user's location
const ipInfo = await getIpInfo()
// Map country to Stripe price ID
// You'll need to create price IDs for each currency in Stripe
const priceId = getPriceIdForCountry(plan, interval, ipInfo.country)
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
// ...
})
return { url: session.url }
})
Additional use cases
Localized content
Show different content or messages based on user's country:
<script setup>
const { data: ipInfo } = await useFetch('/api/ip')
const welcomeMessage = computed(() => {
const messages = {
US: 'Welcome! 🇺🇸',
DE: 'Willkommen! 🇩🇪',
FR: 'Bienvenue! 🇫🇷',
}
return messages[ipInfo.value?.country] || 'Welcome!'
})
</script>
Regional restrictions
Restrict access based on location in server middleware:
export default defineEventHandler(async event => {
const ipInfo = await getIpInfo()
const blockedCountries = ['XX', 'YY']
if (blockedCountries.includes(ipInfo.country)) {
throw createError({
statusCode: 403,
message: 'Service not available in your region',
})
}
})
User preference override
Allow users to manually select their currency/location:
<script setup>
const { data: ipInfo } = await useFetch('/api/ip')
const userCurrency = useCookie('user-currency')
const currency = computed({
get: () => userCurrency.value || getCurrencyFromCountry(ipInfo.value?.country),
set: (value) => { userCurrency.value = value }
})
</script>
<template>
<Select v-model="currency">
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="EUR">EUR</SelectItem>
<SelectItem value="GBP">GBP</SelectItem>
</Select>
</template>
Privacy and performance
GDPR compliance: IP-based geolocation is generally acceptable under GDPR as IPs are used for technical purposes. Include geolocation usage in your privacy policy.
Caching: The free tier provides unlimited API requests, but consider caching IP lookups to improve performance and reduce latency.
Error handling: Always provide fallback values (e.g., default to 'US' or 'USD') if geolocation fails.
Reference
User feedback
Collect bug reports, feature requests and contact messages through built-in forms, validated with Zod and stored in PostgreSQL through Prisma.
Analytics
Track page views and custom events with Umami and Vercel Analytics through one composable that stays safe on the server and when a provider fails.