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:
- Production: routes requests to the
webhookssubdomain; example:https://webhooks.helptail.com/a-unique-path - Development/test: adds a
/webhooksprefix 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?
Want to read me more?
-
Simple Stripe Billing for Rails
Getting paying users has never been simpler with Stripe and Rails. Let's go over the tiny amount of code needed today. -
Requestkit: test and send webhooks and API requests in development
A local HTTP request toolkit that captures webhooks and sends API requests during development—no internet required, your data stays private. -
Organized Configuration in Rails
An improved way to store and use business configuration in your Rails app
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
{{comment}}