Using Subdomains in Rails: Development vs Production

For a current SaaS I am working on I needed to set up a webhook endpoint. I want the production environment to use a subdomain like webhooks.helptail.com, but during development, this needs to work on localhost:3000. So I used some of Rails’ routing features for a solution that adapts based on the environment.

The Challenge with Subdomains Locally

When developing locally, Rails applications typically run on localhost:3000, which doesn’t support subdomains in the same way as (development) domain name, like a .dev TLD.

To solve this problem, a different routing strategy is needed for development versus production environments.

Here’s a clean solution that handles both scenarios elegantly:

# config/routes.rb
constraints Rails.env.production? ? {subdomain: "webhooks"} : {} do
  scope Rails.env.production? ? {} : {path: "webhooks"} do
    post "/:path", to: "workflow/webhooks#create", as: :webhooks
  end
end

This code block does two things:

  1. Production: routes requests to the webhooks subdomain; example: https://webhooks.helptail.com/a-unique-path
  2. Development/test: adds a /webhooks prefix to the path; example: http://localhost:3000/webhooks/a-unique-path

The “magic” happens through the conditional use of Rails’ constraints and scope methods. The constraints method applies the subdomain restriction only in production, while the scope method adds the path prefix only in development.

Using separate subdomains for APIs and webhooks (like webhooks.helptail.com) creates a foundation for growth. This approach lets you scale high-traffic parts of your app independently when customer usage surges, implement targeted security policies without complicating your main application (eg. dashboard.helptail.com) and establishes an architecture that simplifies future migrations as you expand (globally through CDNs).

This same pattern works great for API endpoints too. Many applications use an api subdomain:

constraints Rails.env.production? ? {subdomain: "api"} : {} do
  scope Rails.env.production? ? {} : {path: "api"}, defaults: {format: :json} do
    namespace :v1 do
      resources :users
      resources :posts
    end
  end
end

This creates endpoints like:

  • Production: https://api.example.com/v1/users
  • Development: http://localhost:3000/api/v1/users

What do you think of this solution?

Product-minded Rails notes

Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.

Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

Want to read me more?