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
userstable with unique user identifiers - Build the elm
UserIdentityfrom 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 theUserInfo
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
NoSeatsAvailablewhen 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 raisesValueError) unlessControlStrategy.StartSessionForExpiredSessionis 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) aftervalidate_session, andentitlement.custom_datafromvalidate_user. - Node.js (preview branch):
customDataon the state returned byvalidateSession. - .NET and Java (preview branches): the SDKs parse
custom_datafrom responses; .NET adds aCustomDataproperty toIControlStateandIValidatedSessionState. If you persist state with your ownIUserState/IValidatedSessionStateclasses (see State Management), addIReadOnlyDictionary<string, string> CustomDatato 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) andversion(your product version). - The response is
{"status": 0}on success, or a negative status with anerrormessage, 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.