evoleap-licensing

Ruby SDK for evoleap License Manager (ELM) — license your Ruby and Rails applications using evoleap's cloud licensing system.

Supports server instance registration, per-user session licensing, component checkout/checkin, feature flags, and graceful offline handling.

Installation

Add to your Gemfile:

gem 'evoleap-licensing'

Then run:

bundle install

Quick Start

1. Get Your Credentials

Sign up at the ELM portal and create a product. You'll need:

  • Product ID (GUID) — identifies your product
  • Public Key (PEM file) — used to encrypt API communication
  • License Key — provided to your customers

2. Configure the Gem

Create a Rails initializer at config/initializers/evoleap_licensing.rb:

EvoleapLicensing.configure do |config|
  config.host = "https://elm.evoleap.com"  # default; change for self-hosted/elm Lite
  config.timeout = 60                       # HTTP timeout in seconds
end

Store your public key securely (e.g., config/elm_public_key.pem).

3. Register the Server Instance

Do this once per deployment (e.g., in a Rake task or during setup):

public_key = File.read(Rails.root.join("config", "elm_public_key.pem"))

scm = EvoleapLicensing::ServerControlManager.new(
  product_id: "your-product-guid",
  version: "1.0.0",
  public_key: public_key
)

identity = EvoleapLicensing::InstanceIdentity.from_hardware
result = scm.register(license_key: "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", instance_identity: identity)

if result.success?
  # Persist scm.server_state.to_h as JSON (database, file, etc.)
  File.write("config/server_state.json", scm.server_state.to_h.to_json)
  puts "Instance registered: #{scm.server_state.instance_guid}"
else
  puts "Registration failed: #{result.error_message}"
end

4. Register Users

When a user first accesses your application:

public_key = File.read(Rails.root.join("config", "elm_public_key.pem"))
server_state = EvoleapLicensing::ServerState.new(JSON.parse(File.read("config/server_state.json")))

ucm = EvoleapLicensing::UserControlManager.new(
  product_id: "your-product-guid",
  version: "1.0.0",
  instance_id: server_state.instance_guid,
  public_key: public_key
)

user_identity = EvoleapLicensing::UserIdentity.new("uid" => current_user.id.to_s)

result = ucm.register(
  user_info: { name: current_user.name, email: current_user.email },
  user_identity: user_identity
)

if result.success?
  # Persist ucm.user_state.to_h per user (database JSON column)
  current_user.update!(elm_user_state: ucm.user_state.to_h.to_json)
end

5. Validate Sessions

On each request (via before_action or middleware):

ucm = EvoleapLicensing::UserControlManager.new(
  product_id: "your-product-guid",
  version: "1.0.0",
  instance_id: server_state.instance_guid,
  public_key: public_key,
  user_state: EvoleapLicensing::UserState.new(JSON.parse(current_user.elm_user_state)),
  session_state: EvoleapLicensing::SessionState.new(session[:elm_session_state])
)

user_identity = EvoleapLicensing::UserIdentity.new("uid" => current_user.id.to_s)
validity = ucm.validate_session(user_identity: user_identity)

if validity.valid?
  # Save updated state
  current_user.update!(elm_user_state: ucm.user_state.to_h.to_json)
  session[:elm_session_state] = ucm.session_state.to_h

  # Check feature flags
  if ucm.session_state.features.include?("premium")
    # Enable premium features
  end
else
  # Handle invalid license — see Error Handling below
  handle_license_failure(validity)
end

Component Checkout/Checkin

Components are independently licensable features within your product (seat-based or token-based):

# Check out a component during an active session
result = ucm.check_out_components("ReportGenerator", "DataExport")
if result.success?
  # Component is now checked out for this session
  # result.components and result.component_entitlements have details
else
  puts "Checkout failed: #{result.failure_reason}"
  # :insufficient_tokens, :invalid_component, etc.
end

# Check in when done
ucm.check_in_components("ReportGenerator", "DataExport")

# Query all component statuses
status = ucm.components_status
status.components.each do |comp|
  puts "#{comp['name']}: #{comp['license_model']}"
end

License Info

Get information about the license owner during an active session:

info = ucm.license_info
if info.success?
  puts "Licensed to: #{info.owner_name}"
  puts "Expires: #{info.expiry}"
end

State Persistence

The SDK uses three state objects that must be persisted between requests:

State Scope Where to Store Lifetime
ServerState Per deployment JSON file or database Until re-registration
UserState Per user Database (JSON column) Until re-registration
SessionState Per user session Rails session or Redis Until session ends

All state objects support to_h for serialization and accept a Hash in their constructor:

# Save
json = state.to_h.to_json

# Restore
state = EvoleapLicensing::ServerState.new(JSON.parse(json))

Rails Database Migration Example

class AddElmLicensingToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :elm_user_state, :jsonb, default: {}
  end
end

Error Handling

Connection Failures (Server Unreachable)

When the ELM server is unreachable, the SDK uses grace periods to allow continued operation:

validity = ucm.validate_session(user_identity: user_identity)

if validity.valid?
  if validity.in_validation_failure_grace_period?
    # Working offline — warn the user
    flash[:warning] = "License server unreachable. " \
      "License valid until #{validity.grace_period_expiration.strftime('%Y-%m-%d %H:%M')}"
  end
  # Allow access
else
  if validity.invalid_reason == :service_unreachable
    # Grace period expired or never had a successful validation
    render_service_unavailable
  end
end

Registration Failures

result = scm.register(license_key: key, instance_identity: identity)

unless result.success?
  case result.error_message
  when /Invalid license key/i
    # Customer entered wrong key
    flash[:error] = "The license key is invalid. Please check and try again."
  when /Error contacting/i
    # Network issue during registration
    flash[:error] = "Cannot reach the license server. Please check your connection."
  else
    # Other registration errors (product disabled, activation pending, etc.)
    flash[:error] = "Registration failed: #{result.error_message}"
  end
end

Validation Failures

The invalid_reason on SessionValidity and InstanceValidity tells you exactly what went wrong:

def handle_license_failure(validity)
  case validity.invalid_reason
  when :license_expired
    redirect_to license_expired_path,
      alert: "Your license has expired. Please renew to continue."

  when :no_seats_available
    redirect_to no_seats_path,
      alert: "All licensed seats are in use. Please try again later or contact your admin."

  when :session_revoked
    redirect_to session_revoked_path,
      alert: "Your session was revoked by an administrator."

  when :instance_disabled, :user_disabled
    redirect_to ,
      alert: "Your license has been disabled. Please contact support."

  when :inconsistent_registration, :inconsistent_user
    # Hardware or identity changed — may need re-registration
    redirect_to reregistration_path,
      alert: "License identity mismatch detected. Re-registration may be required."

  when :activation_pending
    redirect_to activation_pending_path,
      alert: "Your license is pending activation. Please check with your administrator."

  when :user_tampering_detected
    # System clock was manipulated
    redirect_to tampering_detected_path,
      alert: "A time inconsistency was detected. Please verify your system clock."

  when :registration_required
    redirect_to registration_path

  when :service_unreachable
    render_service_unavailable

  else
    redirect_to license_error_path,
      alert: "A licensing error occurred. Please try again."
  end
end

Component Checkout Failures

result = ucm.check_out_components("AdvancedReporting")

unless result.success?
  case result.failure_reason
  when :insufficient_tokens
    flash[:error] = "Not enough tokens to use this feature."
  when :invalid_component
    flash[:error] = "This component is not available in your license."
  else
    flash[:error] = "Component checkout failed: #{result.failure_reason}"
  end
end

Grace Period Configuration

Grace periods allow your application to continue working when the license server is temporarily unreachable:

strategy = EvoleapLicensing::ControlStrategy.new(
  # Allow unregistered products to run for 7 days (trial period)
  grace_period_for_unregistered_product: 7 * 24 * 3600,

  # Automatically start a new session when the current one expires
  start_session_for_expired_session: false
)

scm = EvoleapLicensing::ServerControlManager.new(
  product_id: "your-product-guid",
  version: "1.0.0",
  public_key: public_key,
  strategy: strategy
)

The validation failure grace period is set server-side when you register your product. It determines how long a previously-validated instance can operate without contacting the license server.

Example Rails Middleware

For applications that need to check licensing on every request:

# app/middleware/license_check_middleware.rb
class LicenseCheckMiddleware
  SKIP_PATHS = %w[/license /health /assets].freeze

  def initialize(app)
    @app = app
  end

  def call(env)
    request = ActionDispatch::Request.new(env)

    # Skip licensing for certain paths
    return @app.call(env) if SKIP_PATHS.any? { |p| request.path.start_with?(p) }

    # Skip if no authenticated user
    return @app.call(env) unless request.session[:user_id]

    user = User.find_by(id: request.session[:user_id])
    return @app.call(env) unless user

    ucm = build_user_control_manager(user, request.session)
    user_identity = EvoleapLicensing::UserIdentity.new("uid" => user.id.to_s)
    validity = ucm.validate_session(user_identity: user_identity)

    if validity.valid?
      # Persist updated state
      user.update_column(:elm_user_state, ucm.user_state.to_h.to_json)
      request.session[:elm_session_state] = ucm.session_state.to_h
      @app.call(env)
    else
      [403, { "Content-Type" => "text/html" },
       ["License validation failed: #{validity.invalid_reason}"]]
    end
  end

  private

  def build_user_control_manager(user, session)
    public_key = Rails.application.credentials.elm_public_key
    server_state = JSON.parse(File.read(Rails.root.join("config", "server_state.json")))

    EvoleapLicensing::UserControlManager.new(
      product_id: Rails.application.credentials.elm_product_id,
      version: Rails.application.config.app_version,
      instance_id: server_state["instance_guid"],
      public_key: public_key,
      user_state: EvoleapLicensing::UserState.new(
        user.elm_user_state.is_a?(String) ? JSON.parse(user.elm_user_state) : user.elm_user_state
      ),
      session_state: EvoleapLicensing::SessionState.new(session[:elm_session_state])
    )
  end
end

Add to config/application.rb:

config.middleware.use LicenseCheckMiddleware

Feature Flags

Features returned from session validation can be used to gate functionality:

# After validate_session succeeds:
features = ucm.session_state.features

if features.include?("advanced_reporting")
  # Show advanced reporting UI
end

if features.include?("api_access")
  # Allow API access
end

Staging vs. Production

ELM provides a free staging server for development. To switch between environments:

# config/initializers/evoleap_licensing.rb
EvoleapLicensing.configure do |config|
  if Rails.env.production?
    config.host = "https://elm.evoleap.com"
  else
    config.host = "https://staging.elm.evoleap.com"
  end
end

You'll need separate product IDs and public keys for staging and production. Store them in Rails credentials:

# config/credentials.yml.enc
elm:
  staging:
    product_id: "staging-product-guid"
    public_key: |
      -----BEGIN PUBLIC KEY-----
      ...staging key...
      -----END PUBLIC KEY-----
  production:
    product_id: "production-product-guid"
    public_key: |
      -----BEGIN PUBLIC KEY-----
      ...production key...
      -----END PUBLIC KEY-----

API Reference

ServerControlManager

Method Returns Description
register(license_key:, instance_identity:) RegistrationResult Register server instance
validate_instance(instance_identity:) InstanceValidity Validate instance license

UserControlManager

Method Returns Description
register(user_info:, user_identity:) RegistrationResult Register a user
validate_session(user_identity:, requested_duration:) SessionValidity Begin or extend session
end_session Boolean End active session
check_out_components(*names) ComponentCheckoutResult Check out components
check_in_components(*names) ComponentCheckinResult Check in components
components_status ComponentsStatus Get all component statuses
license_info LicenseInfo Get license owner info

Result Types

  • RegistrationResultsuccess?, error_message
  • SessionValidityvalid?, invalid_reason, validity_duration, in_validation_failure_grace_period?, in_unregistered_grace_period?, grace_period_expiration
  • InstanceValidityvalid?, invalid_reason, in_validation_failure_grace_period?, in_unregistered_grace_period?, grace_period_expiration
  • ComponentCheckoutResultsuccess?, failure_reason, components, component_entitlements
  • ComponentCheckinResultsuccess?, failure_reason
  • ComponentsStatussuccess?, components, component_entitlements, error_message
  • LicenseInfosuccess?, owner_name, owner_logo_url, expiry, error_message

License

MIT