Web Application Integration

Complete guide for integrating elm licensing into web applications using the two-tier ServerControlManager and UserControlManager architecture

Two-Tier Architecture Overview

Web applications use a two-tier elm licensing architecture: the web server is registered once as an instance of your product, and each signed-in user is registered and given a licensing session on that instance. Both managers live in the server-side SDKs: the .NET SDK (evoleap.Licensing.Server namespace), the Java SDK (com.evoleap.licensing.Server package), the Python SDK (evoleap_licensing.server package, Preview — not yet in the released Python SDK) and the Ruby gem (EvoleapLicensing module). The Node.js SDK (evoleap-licensing npm package) offers the same flow as functions in its server and user modules (registerServer, register, validateSession, endSession, checkOutComponents). The C++ SDK provides ServerControlManager only.

Important: Authentication Responsibility

elm does not provide user authentication. Your web application must:

  • Implement its own user authentication system or delegate to an Identity Provider (IDP)
  • Maintain its own users table with unique user identifiers
  • Build the elm UserIdentity from a stable, unique value such as your user ID — the server compares it with the identity used at registration
  • Persist each user's elm user state (which contains the elm user ID) with your user record after registration

Architecture Components

1. ServerControlManager

Purpose: Registers the web server instance with a license key

When: Once per deployment; reuse the result afterwards

Provides: The instance ID (read from the server state after registration) used for all user operations

2. UserControlManager

Purpose: Per-user registration, sessions, features and component checkout

When: On login, while the user is active, and on logout

Requires: The instance ID plus the user's saved user and session state

Integration Flow

1. Deployment / Application Startup

Register the web server with ServerControlManager on first run and store the instance ID. Later starts reuse the stored ID.

2. User Login (After Authentication)

Create a UserControlManager with the instance ID and the user's saved state. Register the user if the state is not yet registered, then call the session validation method, which begins an elm session for the user.

3. Session Management

Calling session validation again while the session is active extends it. Read the user's features, check out components, and save the updated state after each call.

4. User Logout

End the elm session so the seat is released and usage is reported accurately, then discard the stored session state.

Server Registration

The first step is registering your web server instance to obtain the instance ID that will be used for all user operations.

Server Registration Overview

Register once and store the instance ID (or, in Ruby, the whole ServerState) so it survives restarts; do not re-register on every start-up. The registration result only reports success or an error message — the instance ID is read from the manager's server state (ServerState.InstanceId, getServerState().getInstanceId(), server_state.instance_guid).

Server Registration (.NET)

using evoleap.Licensing;
using evoleap.Licensing.Server;

// Register once per deployment, then reuse the instance ID - typically a singleton service
public class LicensingService
{
    public Guid ProductId { get; }
    public Version AppVersion { get; } = new Version(1, 0, 0);
    public PublicKeyFile PublicKey { get; }
    public Guid InstanceId { get; private set; }

    private readonly IInstanceIdStore _instanceIdStore;   // your own storage (database, config, ...)

    public LicensingService(IConfiguration config, IInstanceIdStore instanceIdStore)
    {
        ProductId = Guid.Parse(config["Licensing:ProductId"]);
        PublicKey = PublicKeyFile.ReadFrom(config["Licensing:PublicKeyPath"]);
        _instanceIdStore = instanceIdStore;
    }

    // Call during application startup
    public async Task InitializeAsync(string licenseKey)
    {
        var cached = _instanceIdStore.Load();               // Guid? - null on first run
        if (cached.HasValue)
        {
            InstanceId = cached.Value;
            return;
        }

        var serverManager = new ServerControlManager(ProductId, AppVersion, PublicKey);

        // Identify this server; AddHostName / AddAllHardwareKeys are SDK extension methods
        var instanceIdentity = new InstanceIdentity().AddHostName();

        RegistrationResult result = await serverManager.RegisterAsync(licenseKey, instanceIdentity);
        if (!result.Success)
            throw new InvalidOperationException($"Server registration failed: {result.ErrorMessage}");

        // RegistrationResult only carries Success/ErrorMessage; the ID lives in the server state
        InstanceId = serverManager.ServerState.InstanceId;
        _instanceIdStore.Save(InstanceId);
    }

    // One UserControlManager per user request/operation - see User Registration
    public UserControlManager CreateUserControlManager(IUserState savedState = null,
                                                       IValidatedSessionState sessionState = null)
    {
        return new UserControlManager(ProductId, AppVersion, InstanceId, PublicKey,
                                      savedState: savedState, sessionState: sessionState);
    }
}

Server Registration (Java)

import com.evoleap.licensing.HardwareBasedInstanceIdentity;
import com.evoleap.licensing.IValidatedSessionState;
import com.evoleap.licensing.InstanceIdentity;
import com.evoleap.licensing.PublicKeyFile;
import com.evoleap.licensing.RegistrationResult;
import com.evoleap.licensing.Server.IUserState;
import com.evoleap.licensing.Server.ServerControlManager;
import com.evoleap.licensing.Server.UserControlManager;
import com.evoleap.licensing.Utilities.Version;
import java.util.UUID;

// Register once per deployment, then reuse the instance ID
@Service
public class LicensingService {
    private final UUID productId;
    private final Version appVersion = new Version("1.0.0");
    private final PublicKeyFile publicKey;
    private final UUID instanceId;

    public LicensingService(@Value("${licensing.productId}") String productId,
                            @Value("${licensing.publicKeyPath}") String publicKeyPath,
                            @Value("${licensing.licenseKey}") String licenseKey,
                            InstanceIdStore instanceIdStore) throws Exception {   // your own storage
        this.productId = UUID.fromString(productId);
        this.publicKey = PublicKeyFile.readFrom(publicKeyPath);

        UUID cached = instanceIdStore.load();               // null on first run
        if (cached == null) {
            // strategy and savedState may be null
            ServerControlManager serverManager =
                new ServerControlManager(this.productId, appVersion, publicKey, null, null);

            InstanceIdentity identity = HardwareBasedInstanceIdentity.addHostName(new InstanceIdentity());

            // SDK methods return com.evoleap.licensing.Utilities.Task (a java.util.concurrent.Future)
            RegistrationResult result = serverManager.registerAsync(licenseKey, identity).get();
            if (!result.getSuccess()) {
                throw new IllegalStateException("Server registration failed: " + result.getErrorMessage());
            }

            cached = serverManager.getServerState().getInstanceId();
            instanceIdStore.save(cached);
        }
        this.instanceId = cached;
    }

    // One UserControlManager per user request/operation - see User Registration
    public UserControlManager createUserControlManager(IUserState savedState,
                                                       IValidatedSessionState sessionState) throws Exception {
        return new UserControlManager(productId, appVersion, instanceId, publicKey,
                                      null /* ControlStrategy */, savedState, sessionState);
    }
}

Server Registration (Python)

Preview — not yet released. The Python SDK's web server/user API (evoleap_licensing.server) is on an unreleased branch; the released Python SDK supports desktop applications only.

import socket
from uuid import UUID

from evoleap_licensing.InstanceIdentity import InstanceIdentity, InstanceIdentityValue
from evoleap_licensing.server.ServerControlManager import ServerControlManager
from evoleap_licensing.server.UserControlManager import UserControlManager

APP_VERSION = "1.0.0"


class LicensingService:
    """Register once per deployment, then reuse the instance ID."""

    def __init__(self, product_id: str, public_key_pem: str, license_key: str, instance_id_store):
        self.product_id = UUID(product_id)
        self.public_key = public_key_pem          # PEM text of the product's public key

        self.instance_id = instance_id_store.load()   # your own storage; None on first run
        if self.instance_id is None:
            server_manager = ServerControlManager(self.product_id, APP_VERSION, self.public_key)
            identity = InstanceIdentity({"host_name": InstanceIdentityValue(socket.gethostname())})

            result = server_manager.Register(license_key, identity)
            if not result.Success:
                raise RuntimeError(f"Server registration failed: {result.ErrorMessage}")

            # RegistrationResult only has Success/ErrorMessage; the ID lives in the server state
            self.instance_id = server_manager.ServerState.InstanceId
            instance_id_store.save(self.instance_id)

    def create_user_control_manager(self, saved_state=None, session_state=None) -> UserControlManager:
        # Note the argument order: product_id, version, public_key, instance_id
        return UserControlManager(self.product_id, APP_VERSION, self.public_key, self.instance_id,
                                  saved_state=saved_state, session_state=session_state)

Server Registration (Ruby on Rails)

# Gemfile
gem "evoleap-licensing"

# config/initializers/evoleap_licensing.rb
EvoleapLicensing.configure do |config|
  config.host = "https://elm.evoleap.com"   # default
end

# app/services/licensing_service.rb
class LicensingService
  PRODUCT_ID  = Rails.application.credentials.dig(:elm, :product_id)
  APP_VERSION = "1.0.0"
  STATE_PATH  = Rails.root.join("config", "elm_server_state.json")

  def self.public_key
    @public_key ||= File.read(Rails.root.join("config", "elm_public_key.pem"))
  end

  # Run once per deployment (e.g. from a rake task), not on every boot
  def self.register_server!(license_key)
    scm = EvoleapLicensing::ServerControlManager.new(
      product_id: PRODUCT_ID, version: APP_VERSION, public_key: public_key
    )
    result = scm.register(
      license_key: license_key,
      instance_identity: EvoleapLicensing::InstanceIdentity.from_hardware
    )
    raise "Server registration failed: #{result.error_message}" unless result.success?

    File.write(STATE_PATH, scm.server_state.to_h.to_json)
  end

  def self.server_state
    @server_state ||= EvoleapLicensing::ServerState.new(JSON.parse(File.read(STATE_PATH)))
  end

  def self.server_control_manager
    EvoleapLicensing::ServerControlManager.new(
      product_id: PRODUCT_ID, version: APP_VERSION, public_key: public_key,
      server_state: server_state
    )
  end

  # One UserControlManager per user request/operation - see User Registration
  def self.user_control_manager(user_state: nil, session_state: nil)
    EvoleapLicensing::UserControlManager.new(
      product_id: PRODUCT_ID, version: APP_VERSION,
      instance_id: server_state.instance_guid, public_key: public_key,
      user_state: user_state, session_state: session_state
    )
  end
end

User Registration

After server registration, each user must be registered with elm on their first login. This is separate from your application's user authentication.

User Registration Workflow

User registration with elm should happen:

  • After successful application authentication
  • Only if the user's saved elm state is not yet registered
  • Using your internal user ID as the UserIdentity, and the user's name and email (both required) as the UserInfo

Registration records the elm user ID in the user state; it does not start a session. The first session validation call after login begins the session.

User Login and Registration (.NET)

using evoleap.Licensing;
using evoleap.Licensing.Server;

// Your user record keeps the elm user state next to your own fields
public class AppUser
{
    public int Id { get; set; }
    public string DisplayName { get; set; }
    public string Email { get; set; }
    public string ElmUserStateJson { get; set; }   // StoredUserState - see State Management
}

public class LicensingLogin
{
    private readonly LicensingService _licensing;
    private readonly IUserRepository _users;
    private readonly ISessionStateStore _sessions;   // e.g. a distributed cache keyed by user ID

    public LicensingLogin(LicensingService licensing, IUserRepository users, ISessionStateStore sessions)
    {
        _licensing = licensing;
        _users = users;
        _sessions = sessions;
    }

    // Use stable values only: the server rejects later sessions whose identity
    // does not match the identity used at registration (InvalidReason.InconsistentUser)
    public static UserIdentity IdentityFor(AppUser user) =>
        new UserIdentity(new Dictionary<string, string> { { "uid", user.Id.ToString() } });

    // Call AFTER your own authentication has succeeded
    public async Task<SessionValidity> OnLoginAsync(AppUser user)
    {
        var identity = IdentityFor(user);
        var savedState = StoredUserState.FromJson(user.ElmUserStateJson);   // null on first login

        // A new login starts a new elm session, so no session state is passed in
        var userManager = _licensing.CreateUserControlManager(savedState);

        if (!userManager.UserState.Registered)
        {
            // UserInfo requires a non-null name and email
            var registration = await userManager.RegisterAsync(new UserInfo(user.DisplayName, user.Email), identity);
            if (!registration.Success)
                throw new InvalidOperationException($"elm user registration failed: {registration.ErrorMessage}");
            // The elm user ID is now in userManager.UserState.UserId
        }

        // The first ValidateSessionAsync call begins the session
        SessionValidity validity = await userManager.ValidateSessionAsync(identity);

        user.ElmUserStateJson = StoredUserState.From(userManager.UserState).ToJson();
        await _users.UpdateAsync(user);

        if (validity.IsValid)
            await _sessions.SaveAsync(user.Id, StoredSessionState.From(userManager.SessionState));

        return validity;   // reject the login if !validity.IsValid (e.g. NoSeatsAvailable)
    }
}

User Login and Registration (Java)

import com.evoleap.licensing.*;
import com.evoleap.licensing.Server.IUserState;
import com.evoleap.licensing.Server.UserControlManager;
import java.util.Map;

@Service
public class LicensingLogin {
    private final LicensingService licensing;
    private final LicensingStateStore stateStore;   // your own per-user persistence - see State Management

    public LicensingLogin(LicensingService licensing, LicensingStateStore stateStore) {
        this.licensing = licensing;
        this.stateStore = stateStore;
    }

    // Use stable values only: the server rejects later sessions whose identity
    // does not match the identity used at registration (InvalidReason.InconsistentUser)
    public static UserIdentity identityFor(AppUser user) throws Exception {
        return new UserIdentity(Map.of("uid", String.valueOf(user.getId())));
    }

    // Call AFTER your own authentication has succeeded
    public SessionValidity onLogin(AppUser user) throws Exception {
        UserIdentity identity = identityFor(user);
        IUserState savedState = stateStore.loadUserState(user.getId());   // null on first login

        // A new login starts a new elm session, so no session state is passed in
        UserControlManager userManager = licensing.createUserControlManager(savedState, null);

        if (!userManager.getUserState().getRegistered()) {
            UserInfo info = new UserInfo(user.getDisplayName(), user.getEmail());
            RegistrationResult registration = userManager.registerAsync(info, identity).get();
            if (!registration.getSuccess()) {
                throw new IllegalStateException("elm user registration failed: " + registration.getErrorMessage());
            }
            // The elm user ID is now in userManager.getUserState().getUserId()
        }

        // The first validateSessionAsync call begins the session (null = default duration)
        SessionValidity validity = userManager.validateSessionAsync(identity, null).get();

        stateStore.saveUserState(user.getId(), userManager.getUserState());
        if (validity.getIsValid()) {
            stateStore.saveSessionState(user.getId(), userManager.getSessionState());
        }
        return validity;   // reject the login if !validity.getIsValid()
    }
}

User Login and Registration (Python)

Preview — not yet released. The Python SDK's web server/user API (evoleap_licensing.server) is on an unreleased branch; the released Python SDK supports desktop applications only.

from evoleap_licensing.SessionValidity import SessionValidity
from evoleap_licensing.UserIdentity import UserIdentity
from evoleap_licensing.UserInfo import UserInfo


def identity_for(user) -> UserIdentity:
    # Use stable values only: the server rejects later sessions whose identity
    # does not match the identity used at registration (InvalidReason.InconsistentUser)
    return UserIdentity({"uid": str(user.id)})


def on_login(licensing: LicensingService, state_store, user) -> SessionValidity:
    """Call AFTER your own authentication has succeeded."""
    identity = identity_for(user)
    saved_state = state_store.load_user_state(user.id)   # None on first login

    # A new login starts a new elm session, so no session state is passed in
    user_manager = licensing.create_user_control_manager(saved_state=saved_state)

    if not user_manager.UserState.Registered:
        registration = user_manager.Register(UserInfo(user.display_name, user.email), identity)
        if not registration.Success:
            raise RuntimeError(f"elm user registration failed: {registration.ErrorMessage}")
        # The elm user ID is now in user_manager.UserState.UserId

    # The first ValidateSession call begins the session
    validity = user_manager.ValidateSession(identity)

    state_store.save_user_state(user.id, user_manager.UserState)
    if validity.IsValid:
        state_store.save_session_state(user.id, user_manager.SessionState)
    return validity   # reject the login if not validity.IsValid

User Login and Registration (Ruby on Rails)

# db/migrate/xxxx_add_elm_user_state_to_users.rb
class AddElmUserStateToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :elm_user_state, :jsonb, default: {}
  end
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.authenticate(params[:email], params[:password])   # your own authentication
    return redirect_to(login_path, alert: "Invalid credentials") unless user

    ucm = LicensingService.user_control_manager(
      user_state: EvoleapLicensing::UserState.new(user.elm_user_state)
    )
    # Use stable values only: identity must match the one used at registration
    identity = EvoleapLicensing::UserIdentity.new("uid" => user.id.to_s)

    unless ucm.user_state.registered?
      result = ucm.register(user_info: { name: user.name, email: user.email },
                            user_identity: identity)
      raise "elm user registration failed: #{result.error_message}" unless result.success?
      # The elm user ID is now in ucm.user_state.user_guid
    end

    validity = ucm.validate_session(user_identity: identity)   # begins the session
    user.update!(elm_user_state: ucm.user_state.to_h)

    if validity.valid?
      reset_session
      session[:user_id] = user.id
      session[:elm_session_state] = ucm.session_state.to_h
      session[:elm_valid_until] = Time.now.to_i + validity.validity_duration.to_i   # seconds
      redirect_to root_path
    else
      redirect_to login_path, alert: "License check failed: #{validity.invalid_reason}"
    end
  end
end

Session Validation

Once a session has begun, call session validation again to extend it before it expires, and use the same UserControlManager pattern to read features, check out components and end the session at logout.

Session Behavior

  • Without an active session, validation begins one (this can fail with NoSeatsAvailable when the license's concurrent seats are in use); with an active session it extends it.
  • If elm cannot be reached, a previously validated user stays valid during the validation failure grace period (IsInValidationFailureGracePeriod / in_validation_failure_grace_period?). The grace period is configured on the server.
  • Once a session has been ended, the same session state cannot be validated again (the .NET SDK throws InvalidOperationException, the preview Python SDK raises ValueError) unless ControlStrategy.StartSessionForExpiredSession is set. Discard the session state at logout and start fresh at the next login.
  • Each manager allows one licensing call at a time; a concurrent call on the same .NET or Java manager fails with “A licensing request is already in progress.” Create a manager per operation.

Session Validation (.NET)

public class LicensedRequest
{
    private readonly LicensingService _licensing;   // injected, as in LicensingLogin
    private readonly IUserRepository _users;
    private readonly ISessionStateStore _sessions;

    // Call when the session is close to expiring (see Best Practices), not on every request
    public async Task<SessionValidity> RefreshAsync(AppUser user)
    {
        var savedState = StoredUserState.FromJson(user.ElmUserStateJson);
        var sessionState = await _sessions.LoadAsync(user.Id);   // StoredSessionState from login
        var userManager = _licensing.CreateUserControlManager(savedState, sessionState);

        // With an active session, ValidateSessionAsync extends it
        SessionValidity validity = await userManager.ValidateSessionAsync(LicensingLogin.IdentityFor(user));

        user.ElmUserStateJson = StoredUserState.From(userManager.UserState).ToJson();
        await _users.UpdateAsync(user);

        if (validity.IsValid)
        {
            await _sessions.SaveAsync(user.Id, StoredSessionState.From(userManager.SessionState));

            if (validity.IsInValidationFailureGracePeriod)
            {
                // elm could not be reached; access is allowed until validity.GracePeriodExpiration
            }
            return validity;
        }

        switch (validity.InvalidReason)
        {
            case InvalidReason.NoSeatsAvailable:   /* all seats in use */            break;
            case InvalidReason.LicenseExpired:     /* ask the customer to renew */    break;
            case InvalidReason.SessionRevoked:     /* revoked in the elm portal */    break;
            case InvalidReason.UserDisabled:       /* user disabled in elm */         break;
            case InvalidReason.InconsistentUser:   /* identity changed */             break;
            case InvalidReason.ServiceUnreachable: /* grace period exhausted */       break;
        }
        await _sessions.DeleteAsync(user.Id);
        return validity;
    }

    // Features, component checkout and logout use the same UserControlManager pattern
    public async Task<bool> UseReportingAsync(UserControlManager userManager)
    {
        if (!userManager.UserState.Features.Contains("Reporting"))
            return false;

        var checkout = await userManager.CheckOutComponentsAsync("ReportGenerator");
        return checkout.Success;
    }

    public async Task LogoutAsync(AppUser user)
    {
        var userManager = _licensing.CreateUserControlManager(
            StoredUserState.FromJson(user.ElmUserStateJson), await _sessions.LoadAsync(user.Id));
        await userManager.EndSessionAsync();        // frees the seat
        await _sessions.DeleteAsync(user.Id);       // an ended session cannot be validated again
    }
}

Session Validation (Java)

public class LicensedRequest {
    private final LicensingService licensing;
    private final LicensingStateStore stateStore;

    public LicensedRequest(LicensingService licensing, LicensingStateStore stateStore) {
        this.licensing = licensing;
        this.stateStore = stateStore;
    }

    // Call when the session is close to expiring (see Best Practices), not on every request
    public SessionValidity refresh(AppUser user) throws Exception {
        UserControlManager userManager = licensing.createUserControlManager(
            stateStore.loadUserState(user.getId()),
            stateStore.loadSessionState(user.getId()));

        // With an active session, validateSessionAsync extends it
        SessionValidity validity = userManager
            .validateSessionAsync(LicensingLogin.identityFor(user), null).get();

        stateStore.saveUserState(user.getId(), userManager.getUserState());

        if (validity.getIsValid()) {
            stateStore.saveSessionState(user.getId(), userManager.getSessionState());
            if (validity.getIsInValidationFailureGracePeriod()) {
                // elm could not be reached; access is allowed until validity.getGracePeriodExpiration()
            }
            return validity;
        }

        switch (validity.getInvalidReason()) {
            case NoSeatsAvailable:   /* all seats in use */          break;
            case LicenseExpired:     /* ask the customer to renew */ break;
            case SessionRevoked:     /* revoked in the elm portal */ break;
            case UserDisabled:       /* user disabled in elm */      break;
            case InconsistentUser:   /* identity changed */          break;
            default: break;
        }
        stateStore.deleteSessionState(user.getId());
        return validity;
    }

    public boolean useReporting(UserControlManager userManager) throws Exception {
        if (!userManager.getUserState().getFeatures().contains("Reporting")) {
            return false;
        }
        ComponentCheckoutResult checkout = userManager.checkOutComponentsAsync("ReportGenerator").get();
        return checkout.getSuccess();
    }

    public void logout(AppUser user) throws Exception {
        UserControlManager userManager = licensing.createUserControlManager(
            stateStore.loadUserState(user.getId()),
            stateStore.loadSessionState(user.getId()));
        userManager.endSessionAsync().get();          // frees the seat
        stateStore.deleteSessionState(user.getId());  // an ended session cannot be validated again
    }
}

Session Validation (Python)

Preview — not yet released. The Python SDK's web server/user API (evoleap_licensing.server) is on an unreleased branch; the released Python SDK supports desktop applications only.

from evoleap_licensing.enumerations.licensing_enumerations import InvalidReason


def refresh(licensing, state_store, user):
    """Call when the session is close to expiring (see Best Practices), not on every request."""
    user_manager = licensing.create_user_control_manager(
        saved_state=state_store.load_user_state(user.id),
        session_state=state_store.load_session_state(user.id),
    )

    # With an active session, ValidateSession extends it
    validity = user_manager.ValidateSession(identity_for(user))
    state_store.save_user_state(user.id, user_manager.UserState)

    if validity.IsValid:
        state_store.save_session_state(user.id, user_manager.SessionState)
        if validity.IsInValidationFailureGracePeriod:
            pass  # elm unreachable; access is allowed until validity.GracePeriodExpiration
        return validity

    if validity.InvalidReason is InvalidReason.NoSeatsAvailable:
        pass  # all seats in use
    elif validity.InvalidReason is InvalidReason.LicenseExpired:
        pass  # ask the customer to renew
    elif validity.InvalidReason in (InvalidReason.SessionRevoked, InvalidReason.UserDisabled):
        pass  # changed in the elm portal
    state_store.delete_session_state(user.id)
    return validity


def use_reporting(user_manager) -> bool:
    if "Reporting" not in user_manager.UserState.Features:
        return False
    return user_manager.CheckOutComponents("ReportGenerator").Success


def logout(licensing, state_store, user):
    user_manager = licensing.create_user_control_manager(
        saved_state=state_store.load_user_state(user.id),
        session_state=state_store.load_session_state(user.id),
    )
    user_manager.EndSession()                  # frees the seat
    state_store.delete_session_state(user.id)  # an ended session cannot be validated again

Session Validation (Ruby on Rails)

# app/controllers/concerns/elm_licensed.rb
module ElmLicensed
  extend ActiveSupport::Concern

  included do
    before_action :refresh_elm_session
  end

  private

  def elm_user_control_manager
    @elm_ucm ||= LicensingService.user_control_manager(
      user_state: EvoleapLicensing::UserState.new(current_user.elm_user_state),
      session_state: EvoleapLicensing::SessionState.new(session[:elm_session_state])
    )
  end

  # Re-validate only when the session is close to expiring (see Best Practices)
  def refresh_elm_session
    return if session[:elm_valid_until].to_i > 5.minutes.from_now.to_i

    ucm = elm_user_control_manager
    validity = ucm.validate_session(
      user_identity: EvoleapLicensing::UserIdentity.new("uid" => current_user.id.to_s)
    )   # extends the active session
    current_user.update_column(:elm_user_state, ucm.user_state.to_h)

    if validity.valid?
      session[:elm_session_state] = ucm.session_state.to_h
      session[:elm_valid_until] = Time.now.to_i + validity.validity_duration.to_i
      if validity.in_validation_failure_grace_period?
        flash.now[:warning] = "License server unreachable; access allowed until " \
                              "#{validity.grace_period_expiration}"
      end
    else
      session.delete(:elm_session_state)
      case validity.invalid_reason
      when :no_seats_available then redirect_to no_seats_path
      when :license_expired    then redirect_to license_expired_path
      when :session_revoked, :user_disabled then redirect_to account_disabled_path
      else redirect_to license_error_path, alert: "License check failed: #{validity.invalid_reason}"
      end
    end
  end

  def elm_feature?(name)
    EvoleapLicensing::SessionState.new(session[:elm_session_state]).features.include?(name)
  end
end

# Component checkout inside a licensed action
result = elm_user_control_manager.check_out_components("ReportGenerator")
session[:elm_session_state] = elm_user_control_manager.session_state.to_h
head :payment_required unless result.success?   # result.failure_reason, e.g. :insufficient_tokens

# Logout
ucm = elm_user_control_manager
ucm.end_session if ucm.session_state.active?   # end_session raises NoActiveSessionError otherwise
reset_session

State Management

Web applications must persist two pieces of state for each user: the user state (IUserState / UserState: registration, elm user ID, features, grace-period bookkeeping) and the session state (IValidatedSessionState / ValidatedSessionState / SessionState: the active session's auth token and session key). Pass both back to the UserControlManager constructor on the next request. The .NET and Java constructors reject a session state without a user state.

Important: Web State Management Differences

Web applications have unique state management requirements:

  • Per-User Storage: Each user needs their own user state and session state
  • Scalable Storage: Use databases, Redis, or cloud storage rather than local files
  • Session Isolation: User sessions must be isolated from each other
  • State Persistence: Save state after every licensing operation that modifies it

State Storage Strategies

Database Storage

Store state as JSON/BLOB in user tables

Best for: User state, which lives as long as the registration

Redis/Cache Storage

Store session state in a fast cache layer

Best for: Session state, which lives only until logout

Hybrid Approach

User state in the database, session state in the cache or web session

Best for: Most web applications

Web State Management (.NET)

using System.Collections.ObjectModel;
using evoleap.Licensing;
using evoleap.Licensing.Server;
using Newtonsoft.Json;

// The SDK's own state classes are internal, so persist a snapshot that implements the
// public interfaces and pass it back as savedState / sessionState.
public class StoredUserState : IUserState
{
    public Guid UserId { get; set; }
    public bool Registered { get; set; }
    public DateTime? RegisteredAt { get; set; }
    public DateTime FirstLaunchTime { get; set; }
    public DateTime? LastSuccessfulValidationTime { get; set; }
    public int LastValidationStatus { get; set; }
    public TimeSpan GracePeriodForValidationFailures { get; set; }
    public TimeSpan SessionDuration { get; set; }
    public List<DateTime> FailedRegistrationTimes { get; set; } = new List<DateTime>();
    public List<DateTime> FailedValidationTimes { get; set; } = new List<DateTime>();
    public List<string> Features { get; set; } = new List<string>();
    public List<string> FeatureGroups { get; set; } = new List<string>();
    public List<OfflineWebLicenseCheckoutInfo> OfflineCheckouts { get; set; } = new List<OfflineWebLicenseCheckoutInfo>();

    ReadOnlyCollection<DateTime> IControlState.FailedRegistrationTimes => FailedRegistrationTimes.AsReadOnly();
    ReadOnlyCollection<DateTime> IControlState.FailedValidationTimes => FailedValidationTimes.AsReadOnly();
    ReadOnlyCollection<string> IControlState.Features => Features.AsReadOnly();
    ReadOnlyCollection<string> IControlState.FeatureGroups => FeatureGroups.AsReadOnly();

    public static StoredUserState From(IUserState s) => new StoredUserState
    {
        UserId = s.UserId,
        Registered = s.Registered,
        RegisteredAt = s.RegisteredAt,
        FirstLaunchTime = s.FirstLaunchTime,
        LastSuccessfulValidationTime = s.LastSuccessfulValidationTime,
        LastValidationStatus = s.LastValidationStatus,
        GracePeriodForValidationFailures = s.GracePeriodForValidationFailures,
        SessionDuration = s.SessionDuration,
        FailedRegistrationTimes = s.FailedRegistrationTimes.ToList(),
        FailedValidationTimes = s.FailedValidationTimes.ToList(),
        Features = s.Features.ToList(),
        FeatureGroups = s.FeatureGroups.ToList(),
    };

    public string ToJson() => JsonConvert.SerializeObject(this);
    public static StoredUserState FromJson(string json) =>
        string.IsNullOrEmpty(json) ? null : JsonConvert.DeserializeObject<StoredUserState>(json);
}

public class StoredSessionState : IValidatedSessionState
{
    public int SessionState { get; set; }
    public string AuthToken { get; set; }
    public string SessionKey { get; set; }
    public string OfflineCheckoutKey { get; set; }
    public Guid OfflineSessionID { get; set; }
    public List<SessionOfflineTokenCheckoutInfo> OfflineTokenCheckouts { get; set; } = new List<SessionOfflineTokenCheckoutInfo>();

    ReadOnlyCollection<SessionOfflineTokenCheckoutInfo> IValidatedSessionState.OfflineTokenCheckouts => OfflineTokenCheckouts.AsReadOnly();

    public static StoredSessionState From(IValidatedSessionState s) => new StoredSessionState
    {
        SessionState = s.SessionState,
        AuthToken = s.AuthToken,
        SessionKey = s.SessionKey,
        OfflineCheckoutKey = s.OfflineCheckoutKey,
        OfflineSessionID = s.OfflineSessionID,
        OfflineTokenCheckouts = s.OfflineTokenCheckouts?.ToList() ?? new List<SessionOfflineTokenCheckoutInfo>(),
    };
}

Web State Management (Java)

import com.evoleap.licensing.IValidatedSessionState;
import com.evoleap.licensing.Server.IUserState;
import java.util.UUID;

// Your own persistence layer. UserControlManager.getUserState() / getSessionState()
// return the SDK's public UserState and ValidatedSessionState objects; any object that
// implements IUserState / IValidatedSessionState can be passed back to the constructor,
// which copies it.
public interface LicensingStateStore {
    IUserState loadUserState(long userId);                  // null if never registered
    void saveUserState(long userId, IUserState state);      // database, per user

    IValidatedSessionState loadSessionState(long userId);   // null if no active session
    void saveSessionState(long userId, IValidatedSessionState state);   // session store / cache
    void deleteSessionState(long userId);
}

// What must survive between requests:
//   IUserState:             getRegistered(), getUserId(), getRegisteredAt(), getFirstLaunchTime(),
//                           getFeatures(), getFeatureGroups(), getSessionDuration(),
//                           getGracePeriodForValidationFailures(), getLastSuccessfulValidationTime(),
//                           getFailedValidationTimes(), ...
//   IValidatedSessionState: getAuthToken(), getSessionKey(), getSessionState(), ...

Web State Management (Python)

Preview — not yet released. The Python SDK's web server/user API (evoleap_licensing.server) is on an unreleased branch; the released Python SDK supports desktop applications only.

import pickle

from evoleap_licensing.server.UserState import UserState
from evoleap_licensing.states.ValidatedSessionState import ValidatedSessionState


class LicensingStateStore:
    """UserState and ValidatedSessionState are plain Python objects; the SDK's own sample
    persists state with pickle. Only unpickle data your application wrote itself."""

    def __init__(self, db, cache):
        self.db = db        # e.g. a users.elm_user_state BLOB column
        self.cache = cache  # e.g. Redis

    def load_user_state(self, user_id) -> UserState | None:
        blob = self.db.get_elm_user_state(user_id)
        return pickle.loads(blob) if blob else None

    def save_user_state(self, user_id, state: UserState):
        self.db.set_elm_user_state(user_id, pickle.dumps(state))

    def load_session_state(self, user_id) -> ValidatedSessionState | None:
        blob = self.cache.get(f"elm:session:{user_id}")
        return pickle.loads(blob) if blob else None

    def save_session_state(self, user_id, state: ValidatedSessionState):
        self.cache.set(f"elm:session:{user_id}", pickle.dumps(state))

    def delete_session_state(self, user_id):
        self.cache.delete(f"elm:session:{user_id}")

Web State Management (Ruby on Rails)

# Every state object serializes with to_h and is rebuilt from a Hash
#
#   State                          Scope            Where to store
#   EvoleapLicensing::ServerState  per deployment   JSON file or database
#   EvoleapLicensing::UserState    per user         users.elm_user_state (jsonb)
#   EvoleapLicensing::SessionState per login        Rails session or Redis

# Save
current_user.update!(elm_user_state: ucm.user_state.to_h)
session[:elm_session_state] = ucm.session_state.to_h

# Restore
user_state    = EvoleapLicensing::UserState.new(current_user.elm_user_state)
session_state = EvoleapLicensing::SessionState.new(session[:elm_session_state])

# With the default cookie store, SessionState (auth token, features, components)
# travels in the cookie - prefer a server-side session store or Redis
# if the payload grows.

Key Implementation Points

1. Database Schema Design

Add a column to your users table for the serialized elm user state. The elm user ID is part of that state, so a separate column is optional (useful for lookups).

ALTER TABLE users ADD COLUMN elm_user_state TEXT;      -- or JSONB
ALTER TABLE users ADD COLUMN elm_user_guid UUID;       -- optional copy of the elm user ID

2. State Serialization Strategy

.NET: implement the public state interfaces (the SDK's state classes are internal), as shown above. Java: the SDK's UserState and ValidatedSessionState classes are public. Python: pickle the state objects. Ruby: to_h and State.new(hash).

3. Hybrid Storage Approach

User State: Store in the database for persistence across sessions
Session State: Store in Redis/cache or the web session; delete it at logout

4. State Persistence Timing

Save state after every elm SDK operation that modifies it:

  • After user registration
  • After session validation (begin or extend)
  • After component checkout/checkin
  • At logout (delete the session state after ending the session)

5. Performance Considerations

Each validation is a round trip to elm. Cache the session's expiry and validate only when it is close, and use connection pooling for your database and Redis connections.

License Properties (custom_data) Preview

Preview — API v3.5, not yet released. The released backend supports client API up to v3.4, and no released SDK exposes license properties yet. Details may change before release.

With client API v3.5, successful session responses (begin and extend session, validate instance) and validate_user responses include custom_data: the key-value properties set on the customer's license in the elm portal. Use them for per-customer settings such as a plan tier or tenant limits.

  • Ruby (preview branch): ucm.session_state.custom_data (a Hash) after validate_session, and entitlement.custom_data from validate_user.
  • Node.js (preview branch): customData on the state returned by validateSession.
  • .NET and Java (preview branches): the SDKs parse custom_data from responses; .NET adds a CustomData property to IControlState and IValidatedSessionState. If you persist state with your own IUserState / IValidatedSessionState classes (see State Management), add IReadOnlyDictionary<string, string> CustomData to them when you move to such a build.
  • Python: not exposed.
validity = ucm.validate_session(user_identity: identity)
if validity.valid?
  tier = ucm.session_state.custom_data["tier"]   # nil when the license has no such property
end

Checking a User Without a Session Preview

Preview — API v3.5, not yet released. validate_user is not available on the released backend (client API up to v3.4) or in the released Ruby gem (1.0.3.18, API v3.4).

API calls and background jobs act for a user but have no browser session to extend. Client API v3.5 adds validate_user: it asks elm whether a registered user of your instance is still entitled and returns the user's features, feature groups and license properties. It starts no session, uses no seat and does not change the server state, so it is safe to call concurrently. It does record a validation entry, so cache the answer for a few minutes rather than calling it on every request. The Ruby gem's preview branch exposes it as ServerControlManager#validate_user.

Entitlement Check Without a Session (.NET)

// Not available in the .NET SDK. Options:
//  - keep the user's elm session alive and use the persisted UserState.Features, or
//  - call validate_user from a service that uses the Ruby gem (see the Ruby tab)

Entitlement Check Without a Session (Java)

// Not available in the Java SDK.
// Use the persisted IUserState.getFeatures() from the user's last session instead.

Entitlement Check Without a Session (Python)

# Not available in the Python SDK.
# Use the persisted UserState.Features from the user's last session instead.

Entitlement Check Without a Session (Ruby on Rails)

# app/jobs/monthly_report_job.rb - acts for a user without a browser session
class MonthlyReportJob < ApplicationJob
  def perform(user_id)
    user = User.find(user_id)
    user_guid = user.elm_user_state["user_guid"]   # stored by UserState#to_h at registration

    entitlement = Rails.cache.fetch("elm:entitlement:#{user.id}", expires_in: 5.minutes) do
      LicensingService.server_control_manager.validate_user(user_guid: user_guid)
    end

    unless entitlement.valid?
      # :license_expired, :user_disabled, :user_not_registered, :service_unreachable, ...
      Rails.logger.warn("User #{user.id} not entitled: #{entitlement.invalid_reason}")
      return
    end

    generate_report(user) if entitlement.features.include?("Reporting")
    tier = entitlement.custom_data["tier"]   # license properties
  end
end

Web API Products

Products of the Web API type meter usage per call instead of per user session. Their currency-based components are checked out with POST /api/v3.4/auth/check_out_webapi_components (also available under v3.2 and v3.3). There is no SDK wrapper for this endpoint:

  • Authenticate with a customer API token issued for the license, sent as Authorization: Bearer <token>.
  • Send components (a component name or an array of names) and version (your product version).
  • The response is {"status": 0} on success, or a negative status with an error message, for example -14 (insufficient tokens) or -15 (invalid component).
  • Only currency (consumable) components are accepted; session- or token-based components are rejected.

Best Practices

1. Register the server once

Store the instance ID (or Ruby ServerState) and reuse it. Registration is a deployment step, not a start-up step.

2. Keep user identities stable

Build UserIdentity from an immutable key such as your user ID. Values that change (email, username) cause InconsistentUser failures on later sessions.

3. Validate on login, then before expiry

Begin the session at login, record when it expires (from the validity duration), and extend it only when that time is close, not on every HTTP request.

4. Always end sessions

End the elm session at logout (and when your own session times out) so concurrent seats are released, then delete the stored session state.

5. Handle grace periods and failures explicitly

Allow access during the validation failure grace period but warn the user, and map invalid reasons such as NoSeatsAvailable, LicenseExpired, SessionRevoked and UserDisabled to clear messages.

6. Do not share a manager across concurrent requests

Create a UserControlManager per operation from the stored state, and serialize concurrent licensing calls for the same user so state updates are not lost.