Letting Users Add Custom Domains On Heroku

Consider the following use case: you’d like to run a blogging service on Heroku that allows users to sign up and bring their own custom domain. We’ll call your new idea “Me-dium” (that’s probably not taken).

So you’ve got www.me-dium.com setup on Heroku. And your users automatically get a subdomain like username.me-dium.com. But they also want to bring their own domain and CNAME it to username.me-dium.com.

The Heroku Platform requires that any custom domains be registered with their corresponding app. That way, when a request comes into your custom domain, the Heroku router knows where to send the request. This is a pretty simple process when you have 1 or 2 domains, but when you want to give users the ability to add their own domain, you need a way to tell Heroku about these new domains.

We’ll be adding a simple background worker to interact with the Heroku API. We add a column and form field to the user’s profile for domain. Then, when they change this value, we update the Heroku API with the new value.

Let’s first start by adding the Heroku API gem to our project.

gem 'platform-api'

Next, we need an API token to use with our app.

$ heroku plugins:install git@github.com:heroku/heroku-oauth.git
$ heroku authorizations:create -d "Platform API example token"
Created OAuth authorization.
  ID:          2f01aac0-e9d3-4773-af4e-3e510aa006ca
  Description: Platform API example token
  Scope:       global
  Token:       e7dd6ad7-3c6a-411e-a2be-c9fe52ac7ed2

Add the token to your .env file:

HEROKU_API_KEY=e7dd6ad7-3c6a-411e-a2be-c9fe52ac7ed2

Let’s set up the worker first. Since this is an external API call, we’ll send this work off to a ActiveJob worker.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'platform-api'

class HerokuDomainJob < ActiveJob::Base
  queue_as :default

  def perform(domain, action)
    heroku = PlatformAPI.connect_oauth(ENV['HEROKU_API_KEY'])

    begin
      case action
      when "add"
        heroku.domain.create(MY_HEROKU_APP_NAME, "hostname" => domain)
      when "remove"
        heroku.domain.delete(MY_HEROKU_APP_NAME, domain)
      end

    rescue Heroku::API::Errors::RequestFailed => e
      Rails.logger.error "[Heroku Domain Worker] ERROR: #{e}"
    end
  end

end

So this one worker will take two params: the domain in question, and what to do with it (add or remove it). We’ll trigger this worker using an ActiveRecord callback. This method utilizes dirty tracking and we expect a domain column on the users table.

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/models/user.rb

after_save :update_heroku_domains, if: "domain.present?"

...

private
  def update_heroku_domains
    if self.domain_changed? && self.domain_was.present?
      HerokuDomainJob.perform_later(self.domain_was, "remove")
    end
    HerokuDomainJob.perform_later(self.domain, "add")
  end

First, we check to see if there was an existing value and that it has been changed (line 9). If so, we send off a worker job to remove the old value (line 10). Then, we add the new value (line 12).

That’s it! Your app is now setup to interact with the Heroku API and can easily add/remove domains as needed. Good luck on your new blogging platform!

Hi there, I'm Jon.

Writer. Musician. Adventurer. Nerd.

Purveyor of GIFs and dad jokes.