Docs
Docs
Guides

Rails

Serve every customer domain from one Rails app. Let signed hosts past host authorization and resolve the tenant from request.host.

How requests arrive

Apexly forwards each visitor's request to your origin with the original Host header, X-Forwarded-Proto: https, and X-Apexly-Domain-Id, X-Apexly-App-Id, X-Apexly-Timestamp and X-Apexly-Signature.

Verify the signature

X-Apexly-Signature is the hex HMAC-SHA256 of <timestamp>.<host> keyed with your signing secret (whsec_…). Store the secret in your credentials as apexly_signing_secret. This method is tested against the shared vectors on every build.

# lib/apexly_origin.rb
require "openssl"

# True when X-Apexly-Signature is valid for this Host (the edge signs "#{timestamp}.#{host}").
def verify_apexly_origin(host, timestamp, signature, secret, now: Time.now.to_i)
  return false unless timestamp.to_s.match?(/\A\d{1,12}\z/) && signature
  return false if (now - timestamp.to_i).abs > 300

  name = host.to_s.sub(/:\d+\z/, "").downcase
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{name}")
  OpenSSL.secure_compare(expected, signature)
end

Allow your customers' hostnames

Rails' host authorization rejects hosts that are not in config.hosts (always in development, and in production once you add any host). Let requests Apexly signed through, and keep your own hosts listed:

# config/environments/production.rb
require "apexly_origin"

config.hosts << "app.hireflow.com"
config.host_authorization = {
  exclude: ->(request) {
    verify_apexly_origin(
      request.host_with_port,
      request.headers["X-Apexly-Timestamp"],
      request.headers["X-Apexly-Signature"],
      Rails.application.credentials.apexly_signing_secret,
    )
  },
}

Requests for any other host get Rails' "Blocked host" response. Rails already trusts X-Forwarded-Proto from the proxy in front of it, so request.ssl? and URL helpers produce https://careers.acme.com/….

Find the tenant

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_tenant

  private

  def set_tenant
    @tenant = Tenant.find_by(hostname: request.host) || Tenant.find_by!(own_host: request.host)
  end
end

Receive webhooks

Verify Apexly-Signature against request.raw_post before parsing it, with verify_apexly_signature from the Webhooks page.

# app/controllers/apexly_webhooks_controller.rb
class ApexlyWebhooksController < ActionController::API
  def create
    secret = Rails.application.credentials.apexly_signing_secret
    return head(:bad_request) unless verify_apexly_signature(request.headers["Apexly-Signature"], request.raw_post, secret)

    event = JSON.parse(request.raw_post)
    Tenant.mark_live!(event.dig("data", "domain", "metadata", "customer")) if event["type"] == "domain.active"
    head :no_content
  end
end

Try it

Run the Quickstart with a test key, then use the sandbox origin to see the headers your app will receive.

What to read next

Was this helpful?
Edit this page