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)
endAllow 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
endReceive 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
endTry it
Run the Quickstart with a test key, then use the sandbox origin to see the headers your app will receive.