# 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.

```ruby check="origin"
# 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:

```ruby
# 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

```ruby
# 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](/docs/webhooks#verify-the-signature) page.

```ruby
# 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](/docs/quickstart) with a test key, then use the [sandbox origin](/docs/guides/test-mode) to see the headers your app will receive.

- [Webhooks](/docs/webhooks)
- [Test mode](/docs/guides/test-mode)
- [Laravel guide](/docs/guides/laravel)
