Cyrus Stoller home about consulting

Using ngrok with Ruby on Rails

Webhooks provide a powerful way to receive callbacks from an API service. Instead of needing to poll for changes, you can setup an endpoint to react to specific events. For example, when using Twilio, webhooks allow you to respond to incoming text messages. When using Stripe, webhooks allow you to respond to a successful or failed subscription payment. Or, when using Plaid, webhooks allow you to receive new transaction histories.

One of the biggest challenges when getting started with webhooks is setting up your development environment. Because a development server is typically only available on localhost you need to add some additional configuration to make your local web application addressable by the API you are working with.

There are many tools you can use to create a reverse proxy for localhost. I’m most familiar with ngrok, but in the past I’ve also used serveo. In this tutorial, I’m going to walkthrough how I setup my Ruby on Rails applications to work with ngrok.

Sign up for ngrok

First, you need to sign up for an ngrok account. There is a free tier that you can experiment with. The downside of the free version is that each time you start ngrok you are assigned a new subdomain. This means that you have to change your configuration with each API provider whenever you start ngrok locally. There are also paid subscriptions that will allow you to customize the domains that you can use. This tutorial works for both the free and paid plans.

Once you have an auth token, install the ngrok client locally and register your auth token.

$ brew tap caskroom/cask
$ brew cask install ngrok
$ ngrok authtoken <YOUR_AUTH_TOKEN>
$ ngrok http 80 # to start the ngrok reverse proxy manually

Integrating ngrok into your Rails development environment

Install the following gems:

# Gemfile
group :development do
  gem 'ngrok-tunnel'
  gem 'tty-box'
end

Add the following at the end of your puma.rb:

# config/puma.rb
if ENV["RACK_ENV"] == "development"
  begin
    options = {
      addr: ENV.fetch("PORT") { 3000 },
      config: File.join(ENV["HOME"], ".ngrok2", "ngrok.yml"),
    }

    # If you have a paid plan you can create tunnels with custom subdomains
    options[:subdomain] = ENV.fetch("NGROK_SUBDOMAIN") { nil }
    options[:region] = ENV.fetch("NGROK_REGION") { "us" }

    # Create tunnel
    Ngrok::Tunnel.start(options)

    box = TTY::Box.frame(width: 50, height: 10, padding: 2,
      title: {top_left: "<NGROK>", bottom_right: "</NGROK>"},
      style: {fg: :green, bg: :black, border: {fg: :green, bg: :black}}) do
      [
        "STATUS: #{Ngrok::Tunnel.status}",
        "PORT:   #{Ngrok::Tunnel.port}",
        "HTTP:   #{Ngrok::Tunnel.ngrok_url}",
        "HTTPS:  #{Ngrok::Tunnel.ngrok_url_https}"
      ].join("\n")
    end
  rescue => error
    box = TTY::Box.frame(width: 50, height: 5, align: :center, padding: 1,
      title: {top_left: "<NGROK>", bottom_right: "</NGROK>"},
      style: {fg: :red, bg: :black, border: {fg: :red, bg: :black}}) do
      "ERROR: could not create a tunnel ;("
    end
  end
  puts "\n#{box}\n"
end

Now, whenever you start your local development server ngrok will start automatically.

Accepting requests from outside localhost

New in Rails 6 you need to specifically whitelist hosts to defend against DNS rebinding attacks. If you don’t add this configuration, you will receive a Blocked host error.

# config/environments/development.rb
Rails.application.configure do
  ...
  config.hosts << /[a-z0-9]+\.ngrok\.io/
end

Alternative approach

In the code above, I wrote out the specifics of how to integrate Puma and ngrok so that this functionality would continue to work without adding an extra dependency. But, if you have a basic Rails + Puma setup, then you may want consider using puma-ngrok-tunnel instead.

Conclusion

Hopefully this helps you start building web applications that rely on webhooks a little faster. Happy hacking.

Category Tutorial