Kamero

IP Geolocation in Ruby on Rails: Detect Visitor Location

Ruby on Rails powers thousands of production web apps. Adding IP geolocation lets you personalize content, detect timezones, and log visitor geography. Here's how to do it with a free API — no gems required.

Basic Ruby: Net::HTTP

require "net/http"
require "json"

uri = URI("https://geo.kamero.ai/api/geo")
response = Net::HTTP.get(uri)
location = JSON.parse(response)

puts "IP: #{location['ip']}"
puts "City: #{location['city']}"
puts "Country: #{location['country']}"
puts "Timezone: #{location['timezone']}"

With Error Handling

require "net/http"
require "json"

def get_geolocation
  uri = URI("https://geo.kamero.ai/api/geo")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.open_timeout = 3
  http.read_timeout = 5

  response = http.get(uri.path)

  if response.code == "200"
    JSON.parse(response.body)
  else
    nil
  end
rescue StandardError => e
  Rails.logger.error("Geolocation failed: #{e.message}")
  nil
end

Rails Controller Concern

# app/controllers/concerns/geolocatable.rb
module Geolocatable
  extend ActiveSupport::Concern

  private

  def visitor_location
    @visitor_location ||= fetch_location
  end

  def visitor_country
    visitor_location&.dig("country")
  end

  def visitor_timezone
    visitor_location&.dig("timezone") || "UTC"
  end

  def fetch_location
    cache_key = "geo:#{request.remote_ip}"

    Rails.cache.fetch(cache_key, expires_in: 1.hour) do
      uri = URI("https://geo.kamero.ai/api/geo")
      response = Net::HTTP.get_response(uri)
      response.code == "200" ? JSON.parse(response.body) : nil
    end
  rescue StandardError
    nil
  end
end

# Usage in any controller:
class HomeController < ApplicationController
  include Geolocatable

  def index
    @city = visitor_location&.dig("city") || "there"
    @country = visitor_country
  end
end

Rails Middleware

# lib/middleware/geolocation.rb
class GeolocationMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    begin
      uri = URI("https://geo.kamero.ai/api/geo")
      response = Net::HTTP.get_response(uri)
      if response.code == "200"
        env["geo.location"] = JSON.parse(response.body)
      end
    rescue StandardError
      # Silently fail
    end

    @app.call(env)
  end
end

# config/application.rb
config.middleware.use GeolocationMiddleware

# Access in controllers:
# request.env["geo.location"]

Using Faraday (Popular HTTP Client)

# Gemfile: gem "faraday"

require "faraday"

class GeoService
  def self.lookup
    conn = Faraday.new(url: "https://geo.kamero.ai") do |f|
      f.request :json
      f.response :json
      f.options.timeout = 5
    end

    response = conn.get("/api/geo")
    response.success? ? response.body : nil
  rescue Faraday::Error => e
    Rails.logger.warn("Geo lookup failed: #{e.message}")
    nil
  end
end

# Usage:
location = GeoService.lookup
puts location["city"] if location

Background Job: Enrich User Records

# app/jobs/enrich_user_location_job.rb
class EnrichUserLocationJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    location = GeoService.lookup

    if location
      user.update(
        detected_country: location["country"],
        detected_city: location["city"],
        detected_timezone: location["timezone"]
      )
    end
  end
end

# Trigger on sign-up:
after_create :enrich_location
def enrich_location
  EnrichUserLocationJob.perform_later(id)
end

View Helper

# app/helpers/geo_helper.rb
module GeoHelper
  def local_time(time, format: :short)
    tz = controller.try(:visitor_timezone) || "UTC"
    time.in_time_zone(tz).to_fs(format)
  end

  def visitor_greeting
    city = controller.try(:visitor_location)&.dig("city")
    city ? "Welcome from #{city}!" : "Welcome!"
  end
end

Works with Any Ruby HTTP Client

Standard JSON API. No gem needed, no API key.

View Documentation →