Server Application Integration
Licensing server applications, services and daemons with
ServerControlManager
Server Licensing Model
A server application licenses the server instance, not the people using it:
- Instance registration: Each server installation registers once with a license
key and a hardware-based
InstanceIdentity. elm returns an instance ID that the SDK keeps in the server state. - Instance validation, not sessions: The server re-validates its instance periodically. Instance validation does not open a session or consume a seat.
- Users are optional: If the server must also license individual users (a web
app or SaaS), register them with
UserControlManageras described in the Web Applications guide, or (preview, API v3.5) check a user without a session usingvalidate_user. - Grace periods: If elm is unreachable, a previously validated instance stays valid for the validation-failure grace period configured on the license.
Server Licensing Architecture
The C#, Java, C++, JavaScript and Ruby SDKs provide a ServerControlManager for server
products (Python's is in preview). It holds the product
ID, version, public key and the persisted server state, and exposes two core operations:
register and validate instance.
Key Differences from Desktop/Web Applications
1. One Control Manager per Server
A desktop application uses ControlManager, which registers and opens sessions
for a user on a machine. A server uses ServerControlManager, which only
registers and validates the instance. Web applications add a
UserControlManager per user on top of the server instance.
2. Instance-Based Licensing
The instance identity (host name, CPU identifier, volume hash, IP and MAC addresses when
using the hardware helpers) is sent at registration and on every validation. If it no longer
matches the registered identity, validation fails with
InconsistentRegistration.
3. Periodic Validation
Validate the instance at startup and then on a timer to pick up revocation, expiry and feature changes made in the elm portal.
4. One Call at a Time
The C#, Java and C++ control managers reject overlapping calls: a second call made while one
is in progress throws ("A licensing request is already in progress."). Run
validation from a single timer, or guard calls with a lock.
What the SDK Sends
All calls are encrypted with your product's public key and posted to
{host}/api/v{version}/auth/<method>.
register_instance: product ID, license key and instance identity. Returns the instance ID and the license's validation grace period.validate_instance: product ID, instance ID, application version and instance identity. elm checks that the license is current, the instance is enabled and activated, the instance is not checked out for offline use, and the identity matches. Returns the status, enabled features and feature groups, and the grace period. (Preview: API v3.5 also returns the license's custom data.)validate_userPreview (API v3.5, not yet released): product ID, instance ID, user ID and version. See Per-User Checks.
Simple Control Flow
Restore the persisted server state (none on first run)
Only if the state is not registered
Start serving if valid; save state
Re-validate on a timer; save state
Warn in grace period, stop when invalid
Server Registration
Registration happens once per installation. On later starts, construct the control manager with
the saved state and go straight to validation. RegistrationResult only reports
success and an error message; the new instance ID is read from the server state.
Server Registration (.NET)
using evoleap.Licensing;
using evoleap.Licensing.Server;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Threading.Tasks;
public class ServerLicensing
{
private static readonly Guid ProductId = new Guid("00000000-0000-0000-0000-000000000000"); // your product ID
private static readonly Version AppVersion = new Version(1, 0, 0);
private readonly string _statePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"MyServerApp", "elm-server-state.json");
public ServerControlManager Manager { get; private set; }
public InstanceIdentity Identity { get; private set; }
public async Task<bool> InitializeAsync(string licenseKey)
{
var publicKey = PublicKeyFile.ReadFrom("elm-public-key.txt");
Manager = new ServerControlManager(ProductId, AppVersion, publicKey,
controlStrategy: null, savedState: LoadState()); // savedState is null on first run
// Host name, CPU identifier, volume hash, IP and MAC addresses
Identity = new InstanceIdentity().AddAllHardwareKeys();
if (!Manager.ServerState.Registered)
{
RegistrationResult registration = await Manager.RegisterAsync(licenseKey, Identity);
SaveState(); // failed attempts are recorded in the state as well
if (!registration.Success)
{
Console.Error.WriteLine($"Registration failed: {registration.ErrorMessage}");
return false;
}
Console.WriteLine($"Registered as instance {Manager.ServerState.InstanceId}");
}
InstanceValidity validity = await Manager.ValidateInstanceAsync(Identity);
SaveState();
return validity.IsValid;
}
private IServerState LoadState() =>
File.Exists(_statePath)
? JsonConvert.DeserializeObject<PersistedServerState>(File.ReadAllText(_statePath))
: null;
public void SaveState()
{
Directory.CreateDirectory(Path.GetDirectoryName(_statePath));
var json = JsonConvert.SerializeObject(PersistedServerState.From(Manager.ServerState), Formatting.Indented);
var temp = _statePath + ".tmp";
File.WriteAllText(temp, json);
File.Move(temp, _statePath, overwrite: true); // atomic replace
}
}
The SDK's own ServerState class is internal, so persist a copy through your own
IServerState implementation:
using evoleap.Licensing;
using evoleap.Licensing.Desktop; // OfflineCheckoutInfo
using evoleap.Licensing.Server;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
public class PersistedServerState : IServerState
{
// Matches the released IControlState. The unreleased v3.5 SDK adds a CustomData member to it.
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 Guid InstanceId { 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<OfflineCheckoutInfo> OfflineCheckouts { get; set; } = new List<OfflineCheckoutInfo>();
ReadOnlyCollection<DateTime> IControlState.FailedRegistrationTimes => FailedRegistrationTimes.AsReadOnly();
ReadOnlyCollection<DateTime> IControlState.FailedValidationTimes => FailedValidationTimes.AsReadOnly();
ReadOnlyCollection<string> IControlState.Features => Features.AsReadOnly();
ReadOnlyCollection<string> IControlState.FeatureGroups => FeatureGroups.AsReadOnly();
ReadOnlyCollection<OfflineCheckoutInfo> IServerState.OfflineCheckouts => OfflineCheckouts.AsReadOnly();
public static PersistedServerState From(IServerState s) => new PersistedServerState
{
Registered = s.Registered,
RegisteredAt = s.RegisteredAt,
FirstLaunchTime = s.FirstLaunchTime,
LastSuccessfulValidationTime = s.LastSuccessfulValidationTime,
LastValidationStatus = s.LastValidationStatus,
GracePeriodForValidationFailures = s.GracePeriodForValidationFailures,
InstanceId = s.InstanceId,
FailedRegistrationTimes = s.FailedRegistrationTimes.ToList(),
FailedValidationTimes = s.FailedValidationTimes.ToList(),
Features = s.Features.ToList(),
FeatureGroups = s.FeatureGroups.ToList(),
OfflineCheckouts = s.OfflineCheckouts.ToList()
};
}
Server Registration (Java)
import com.evoleap.licensing.HardwareBasedInstanceIdentity;
import com.evoleap.licensing.InstanceIdentity;
import com.evoleap.licensing.InstanceValidity;
import com.evoleap.licensing.PublicKeyFile;
import com.evoleap.licensing.RegistrationResult;
import com.evoleap.licensing.Server.IServerState;
import com.evoleap.licensing.Server.ServerControlManager;
import com.evoleap.licensing.Utilities.Version;
import java.util.UUID;
public class ServerLicensing {
private static final UUID PRODUCT_ID = UUID.fromString("00000000-0000-0000-0000-000000000000");
private ServerControlManager manager;
private InstanceIdentity identity;
public ServerControlManager getManager() { return manager; }
public InstanceIdentity getIdentity() { return identity; }
public boolean initialize(String licenseKey) throws Exception {
PublicKeyFile publicKey = PublicKeyFile.readFrom("elm-public-key.txt");
IServerState savedState = loadState(); // your IServerState implementation; null on first run
// A null ControlStrategy uses the defaults
manager = new ServerControlManager(PRODUCT_ID, new Version("1.0.0"), publicKey, null, savedState);
// Host name, CPU identifier, volume hash, IP and MAC addresses
identity = HardwareBasedInstanceIdentity.addAllHardwareKeys(new InstanceIdentity());
if (!manager.getServerState().getRegistered()) {
// The *Async methods return a Task, which is a java.util.concurrent.Future
RegistrationResult registration = manager.registerAsync(licenseKey, identity).get();
saveState(manager.getServerState());
if (!registration.getSuccess()) {
System.err.println("Registration failed: " + registration.getErrorMessage());
return false;
}
System.out.println("Registered as instance " + manager.getServerState().getInstanceId());
}
InstanceValidity validity = manager.validateInstanceAsync(identity).get();
saveState(manager.getServerState());
return validity.getIsValid();
}
private IServerState loadState() {
// Read your persisted copy of the IServerState fields (JSON file, database, ...)
return null;
}
void saveState(IServerState state) throws Exception {
// Copy getRegistered(), getRegisteredAt(), getFirstLaunchTime(), getFailedRegistrationTimes(),
// getFailedValidationTimes(), getLastSuccessfulValidationTime(), getLastValidationStatus(),
// getFeatures(), getFeatureGroups(), getGracePeriodForValidationFailures(), getInstanceId()
// and getOfflineCheckouts() into your own serializable IServerState implementation.
}
}
Server Registration (Python) Preview
Preview — API v3.5, not yet released. The released Python SDK has no
ServerControlManager; this example uses the server support on the unreleased
feature/license-properties branch.
import os
import pickle
import socket
import uuid
from uuid import UUID
from evoleap_licensing.InstanceIdentity import InstanceIdentity, InstanceIdentityValue
from evoleap_licensing.server.ServerControlManager import ServerControlManager
PRODUCT_ID = UUID("00000000-0000-0000-0000-000000000000")
STATE_PATH = "/var/lib/myserverapp/elm-server-state.pickle"
def build_instance_identity() -> InstanceIdentity:
# The Python SDK has no hardware helper; choose values that are stable for this server
return InstanceIdentity({
"host": InstanceIdentityValue(socket.gethostname(), case_sensitive=False),
"mac": InstanceIdentityValue(format(uuid.getnode(), "012x")),
})
def load_state():
if os.path.exists(STATE_PATH):
with open(STATE_PATH, "rb") as f:
return pickle.load(f)
return None
def save_state(manager: ServerControlManager):
os.makedirs(os.path.dirname(STATE_PATH), exist_ok=True)
tmp = STATE_PATH + ".tmp"
with open(tmp, "wb") as f:
pickle.dump(manager.ServerState, f)
os.replace(tmp, STATE_PATH) # atomic replace
def initialize(license_key: str):
with open("elm-public-key.txt") as f:
public_key = f.read() # the public key text
identity = build_instance_identity()
manager = ServerControlManager(PRODUCT_ID, "1.0.0", public_key,
instance_identity=identity,
saved_state=load_state())
if not manager.ServerState.Registered:
registration = manager.Register(license_key, identity)
save_state(manager)
if not registration.Success:
print(f"Registration failed: {registration.ErrorMessage}")
return None
print(f"Registered as instance {manager.ServerState.InstanceId}")
validity = manager.ValidateInstance(identity)
save_state(manager)
return manager if validity.IsValid else None
Server Registration (C++)
#include <ServerControlManager.h>
#include <HardwareBasedInstanceIdentity.h>
#include <ConnectionSettings.h>
#include <fstream>
#include <iostream>
#include <sstream>
class ServerLicensing {
public:
ServerControlManager* manager = nullptr;
InstanceIdentity identity;
bool Initialize(const std::string& licenseKey) {
auto productId = uuids::uuid::from_string("00000000-0000-0000-0000-000000000000").value();
std::ifstream keyFile("elm-public-key.txt");
std::stringstream keyText;
keyText << keyFile.rdbuf();
PublicKeyFile* publicKey = PublicKeyFile::Create(keyText.str());
IServerState* savedState = LoadState(); // your IServerState implementation; nullptr on first run
manager = new ServerControlManager(productId, Version("1.0.0"), publicKey,
nullptr /* default ControlStrategy */, savedState);
// Host name, CPU identifier, volume hash, IP and MAC addresses
HardwareBasedInstanceIdentity::AddAllHardwareKeys(identity);
if (!manager->get_ServerState()->get_Registered()) {
RegistrationResult registration = manager->Register(licenseKey, &identity);
SaveState(manager->get_ServerState());
if (!registration.get_Success()) {
std::cerr << "Registration failed: " << registration.get_ErrorMessage() << std::endl;
return false;
}
std::cout << "Registered as instance "
<< uuids::to_string(manager->get_ServerState()->get_InstanceId()) << std::endl;
}
InstanceValidity validity = manager->ValidateInstance(&identity);
SaveState(manager->get_ServerState());
return validity.get_IsValid();
}
private:
IServerState* LoadState(); // read your persisted copy of the state
void SaveState(IServerState* state); // copy the get_* values of IServerState to storage
};
Server Registration (Ruby)
require "evoleap_licensing"
require "json"
STATE_PATH = "config/elm_server_state.json"
EvoleapLicensing.configure do |config|
config.host = "https://elm.evoleap.com" # the default
end
saved = File.exist?(STATE_PATH) ? JSON.parse(File.read(STATE_PATH)) : nil
scm = EvoleapLicensing::ServerControlManager.new(
product_id: "00000000-0000-0000-0000-000000000000",
version: "1.0.0",
public_key: File.read("config/elm_public_key.pem"),
server_state: EvoleapLicensing::ServerState.new(saved)
)
identity = EvoleapLicensing::InstanceIdentity.from_hardware
save_state = lambda do
File.write("#{STATE_PATH}.tmp", JSON.pretty_generate(scm.server_state.to_h))
File.rename("#{STATE_PATH}.tmp", STATE_PATH) # atomic replace
end
unless scm.server_state.registered?
result = scm.register(license_key: ENV.fetch("ELM_LICENSE_KEY"), instance_identity: identity)
save_state.call
abort "Registration failed: #{result.error_message}" unless result.success?
puts "Registered as instance #{scm.server_state.instance_guid}"
end
validity = scm.validate_instance(instance_identity: identity)
save_state.call
abort "License not valid: #{validity.invalid_reason}" unless validity.valid?
Periodic Validation
Validate the instance at startup and then periodically to detect remote changes such as revocation, expiry or feature updates. An interval of 30-60 minutes is a reasonable default. Persist the state after every call: validation updates features, failure times and the last successful validation time that the grace period is measured from.
Why Periodic Validation is Important
1. Disable and Expiry Detection
If the license expires or the instance is disabled in the portal, the next validation
returns LicenseExpired or InstanceDisabled.
2. Feature Updates
Each successful validation replaces the features and feature groups in the server state with the license's current entitlements.
3. Usage Records
elm records every validation, which feeds the usage reports in the portal.
Interpreting InstanceValidity
Validation Outcomes
- Valid:
IsValidis true. Keep serving. - elm unreachable, within grace:
IsValidis true andIsInValidationFailureGracePeriodis true;GracePeriodExpirationsays when it runs out. The SDK handles this, so network errors need no special code. Warn operators. - elm unreachable, grace exhausted: invalid with
ServiceUnreachable. - Expired or disabled:
LicenseExpired,InstanceDisabled. Stop serving. - Hardware changed:
InconsistentRegistration. The identity no longer matches the registration; the vendor can allow a hardware update in the portal, or the server must be re-registered. - Other:
InstanceNotRegistered,ActivationPending(license requires email activation),InstanceAlreadyCheckedOut(checked out for offline use),UserTamperingDetected(clock manipulation).
Periodic Validation (.NET)
using evoleap.Licensing;
using System;
using System.Threading;
using System.Threading.Tasks;
public sealed class LicenseMonitor : IDisposable
{
private readonly ServerLicensing _licensing;
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); // the SDK rejects overlapping calls
private readonly Timer _timer;
public LicenseMonitor(ServerLicensing licensing)
{
_licensing = licensing;
_timer = new Timer(async _ => await CheckAsync(), null,
TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
}
public async Task<bool> CheckAsync()
{
await _gate.WaitAsync();
try
{
InstanceValidity validity = await _licensing.Manager.ValidateInstanceAsync(_licensing.Identity);
_licensing.SaveState();
if (validity.IsValid)
{
if (validity.IsInValidationFailureGracePeriod)
Console.WriteLine($"WARNING: elm unreachable; grace period ends {validity.GracePeriodExpiration:u}");
return true;
}
switch (validity.InvalidReason)
{
case InvalidReason.ServiceUnreachable: // grace period exhausted
case InvalidReason.LicenseExpired:
case InvalidReason.InstanceDisabled:
case InvalidReason.UserTamperingDetected:
StopServing(validity.InvalidReason);
break;
case InvalidReason.InconsistentRegistration: // hardware no longer matches
case InvalidReason.InstanceNotRegistered:
RequestReRegistration(validity.InvalidReason);
break;
default: // ActivationPending, InstanceAlreadyCheckedOut, ...
StopServing(validity.InvalidReason);
break;
}
return false;
}
finally
{
_gate.Release();
}
}
private void StopServing(InvalidReason reason) { /* drain requests, then stop the host */ }
private void RequestReRegistration(InvalidReason reason) { /* alert operators */ }
public void Dispose() => _timer.Dispose();
}
Periodic Validation (Java)
import com.evoleap.licensing.InstanceValidity;
import com.evoleap.licensing.InvalidReason;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class LicenseMonitor {
private final ServerLicensing licensing;
// A single-threaded scheduler never overlaps calls, which the SDK would reject
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public LicenseMonitor(ServerLicensing licensing) {
this.licensing = licensing;
scheduler.scheduleAtFixedRate(this::check, 30, 30, TimeUnit.MINUTES);
}
void check() {
try {
InstanceValidity validity =
licensing.getManager().validateInstanceAsync(licensing.getIdentity()).get();
licensing.saveState(licensing.getManager().getServerState());
if (validity.getIsValid()) {
if (validity.getIsInValidationFailureGracePeriod()) {
System.out.println("WARNING: elm unreachable; grace period ends "
+ validity.getGracePeriodExpiration());
}
return;
}
switch (validity.getInvalidReason()) {
case InconsistentRegistration: // hardware no longer matches
case InstanceNotRegistered:
requestReRegistration(validity.getInvalidReason());
break;
default: // ServiceUnreachable (grace exhausted), LicenseExpired,
stopServing(validity.getInvalidReason()); // InstanceDisabled, ...
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void stopServing(InvalidReason reason) { /* drain requests, then stop */ }
private void requestReRegistration(InvalidReason reason) { /* alert operators */ }
public void shutdown() { scheduler.shutdown(); }
}
Periodic Validation (Python) Preview
Preview — API v3.5, not yet released. The released Python SDK has no
ServerControlManager; this example uses the server support on the unreleased
feature/license-properties branch.
import threading
from evoleap_licensing.enumerations.licensing_enumerations import InvalidReason
class LicenseMonitor:
def __init__(self, manager, identity, interval_seconds=1800):
self._manager = manager
self._identity = identity
self._interval = interval_seconds
self._lock = threading.Lock() # the manager mutates its state; one call at a time
self._stop = threading.Event()
threading.Thread(target=self._loop, daemon=True).start()
def _loop(self):
while not self._stop.wait(self._interval):
self.check()
def check(self) -> bool:
with self._lock:
validity = self._manager.ValidateInstance(self._identity)
save_state(self._manager) # from the registration example
if validity.IsValid:
if validity.IsInValidationFailureGracePeriod:
print(f"WARNING: elm unreachable; grace period ends {validity.GracePeriodExpiration}")
return True
if validity.InvalidReason in (InvalidReason.InconsistentRegistration,
InvalidReason.InstanceNotRegistered):
request_re_registration(validity.InvalidReason) # your handler: alert operators
else:
# ServiceUnreachable (grace exhausted), LicenseExpired, InstanceDisabled, ...
stop_serving(validity.InvalidReason) # your handler: drain requests, then stop
return False
def shutdown(self):
self._stop.set()
Periodic Validation (C++)
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
class LicenseMonitor {
public:
explicit LicenseMonitor(ServerLicensing& licensing) : licensing(licensing) {
worker = std::thread([this] {
std::unique_lock<std::mutex> lock(mutex);
// One thread makes every call; the manager throws std::logic_error on overlapping calls
while (!cv.wait_for(lock, std::chrono::minutes(30), [this] { return stopping.load(); })) {
Check();
}
});
}
~LicenseMonitor() {
stopping = true;
cv.notify_all();
if (worker.joinable()) worker.join();
}
bool Check() {
InstanceValidity validity = licensing.manager->ValidateInstance(&licensing.identity);
// persist licensing.manager->get_ServerState() here
if (validity.get_IsValid()) {
if (validity.get_IsInValidationFailureGracePeriod()) {
auto left = validity.get_GracePeriodExpiration() - std::chrono::system_clock::now();
std::cout << "WARNING: elm unreachable; grace period ends in "
<< std::chrono::duration_cast<std::chrono::hours>(left).count() << "h" << std::endl;
}
return true;
}
switch (validity.get_InvalidateReason()) { // note: the C++ getter is spelled get_InvalidateReason
case InvalidReason::InconsistentRegistration: // hardware no longer matches
case InvalidReason::InstanceNotRegistered:
RequestReRegistration();
break;
default: // ServiceUnreachable, LicenseExpired, InstanceDisabled, ...
StopServing();
}
return false;
}
private:
void StopServing();
void RequestReRegistration();
ServerLicensing& licensing;
std::thread worker;
std::mutex mutex;
std::condition_variable cv;
std::atomic<bool> stopping{false};
};
Periodic Validation (Ruby)
LICENSE_LOCK = Mutex.new # validate_instance updates scm.server_state; one call at a time
def check_license(scm, identity, save_state)
validity = LICENSE_LOCK.synchronize do
scm.validate_instance(instance_identity: identity).tap { save_state.call }
end
if validity.valid?
if validity.in_validation_failure_grace_period?
warn "elm unreachable; grace period ends #{validity.grace_period_expiration}"
end
return true
end
case validity.invalid_reason
when :inconsistent_registration, :instance_not_registered
request_re_registration(validity.invalid_reason) # hardware changed or registration lost
else
stop_serving(validity.invalid_reason) # :service_unreachable, :license_expired, :instance_disabled, ...
end
false
end
# Re-validate every 30 minutes in a background thread
Thread.new do
loop do
sleep 30 * 60
check_license(scm, identity, save_state)
end
end
Per-User Checks Preview
Preview — API v3.5, not yet released. validate_user needs client API v3.5 on the
elm service and an unreleased version of the Ruby SDK; released services top out at API v3.4.
Until then, check users through UserControlManager sessions.
A server that acts for its users (API calls, background jobs) cannot hold a licensing session for
each of them. API v3.5 adds validate_user: elm answers whether a user registered on
this server instance is entitled to the product right now.
What It Does
It checks the license, the instance, and that the user is registered on this instance and enabled. It starts no session, issues no token, uses no seat and does not change the server state, so concurrent calls are safe. elm records a validation entry, as for every validation.
Prerequisites
The server instance must be registered, and the user must have been registered on it with
UserControlManager (see the Web Applications
guide). Pass the user ID from the user state (user_state.user_guid in Ruby).
A user unknown to elm returns :user_not_registered; one registered on a
different instance returns :inconsistent_user. Web API products cannot be
validated this way.
No Grace Period
Unlike instance validation, the result is a plain answer. If elm is unreachable you get
:service_unreachable and your code decides what to do. Cache answers for a few
minutes rather than calling on every request.
Checking a User Without a Session (Ruby)
entitlement = Rails.cache.fetch("elm:user:#{user.id}", expires_in: 5.minutes) do
scm.validate_user(user_guid: user.elm_user_guid) # => EvoleapLicensing::UserEntitlement
end
if entitlement.valid?
entitlement.features # => ["premium", ...]
entitlement.feature_groups
entitlement.custom_data # => the license's key-value properties
else
entitlement.invalid_reason # => :license_expired, :user_disabled, :service_unreachable, ...
entitlement.error_message
end
When released, validate_user will be available only in the Ruby SDK
(evoleap-licensing). Other SDKs check users through UserControlManager
sessions.
License Properties (Custom Data) Preview
Preview — API v3.5, not yet released. License properties and custom_data exist only
on unreleased SDK and service branches; released SDKs and services (API v3.4 and earlier)
do not return them.
Vendors can attach key-value properties to a license in the elm portal. From API v3.5, elm
returns them as custom_data on successful instance validations, user session
responses and validate_user. Use them for per-customer settings such as tenant
limits or deployment options.
Where Server Code Can Read Them
- Ruby:
UserEntitlement#custom_datafromvalidate_user, anducm.session_state.custom_dataafter a user session.ServerStatedoes not store custom data, sovalidate_instancedoes not expose it. - C#, Java, Python, C++: custom data is parsed from user and app session
responses (for example
IValidatedSessionState.CustomDatain .NET), not fromServerControlManagervalidation. The .NETIServerStatecarries aCustomDataproperty throughIControlState, but instance validation does not fill it.
Property values are strings. Treat a missing key as "not set", since older API versions return no custom data at all.
State Management
A server persists one state object: the server state (IServerState), which holds
the registration, instance ID, features, validation history and any offline checkouts. Without
it, the server cannot validate after a restart and the grace period cannot be computed.
Simple State Persistence Pattern
1. Choose a Serializable Form
In C#, Java and C++ the SDK's concrete state class is internal or not serializable; implement
IServerState yourself and copy the values (see the C# example above). Python
state objects can be pickled (Python server support is in preview). The Ruby ServerState has to_h and
accepts that hash in its constructor.
2. Save After Each Operation
Persist after registration (including failed attempts), after every validation and after offline checkouts or check-ins.
3. Load on Startup
Pass the saved state to the ServerControlManager constructor. If the saved state
is not marked registered, the instance ID is ignored and the server must register again.
State Management Best Practices
- Atomic Writes: Write to a temporary file and rename it over the old one to prevent corruption
- One Writer: With several processes on one machine (workers, web server processes), let one process own registration and validation, or share the state through a database with locking
- Validate on Load: Treat an unreadable state file as missing and re-register rather than crashing
- Secure Storage: Protect state files with appropriate file permissions
Deployment Scenarios
Server applications may be deployed in various network environments, including isolated ones without internet access.
Standard Deployment
Internet-Connected Environment
By default the SDKs call the elm licensing service at https://elm.evoleap.com.
Restricted Network Environment
Reverse Proxy Configuration
For environments with restricted internet access, run a reverse proxy inside your network that forwards to elm, and point the SDK at it before creating the control manager.
Reverse Proxy Configuration (.NET)
using evoleap.Licensing;
ConnectionSettings.Host = "https://internal-proxy.company.com"; // your internal proxy
var manager = new ServerControlManager(productId, version, publicKey, savedState: savedState);
Reverse Proxy Configuration (Java)
import com.evoleap.licensing.ConnectionSettings;
ConnectionSettings.setHost("https://internal-proxy.company.com"); // your internal proxy
ServerControlManager manager = new ServerControlManager(productId, version, publicKey, null, savedState);
Reverse Proxy Configuration (Python) Preview
Preview — API v3.5, not yet released. The released Python SDK has no
ServerControlManager; this example uses the server support on the unreleased
feature/license-properties branch.
from evoleap_licensing.webservice.ConnectionSettings import ConnectionSettings
ConnectionSettings.SetHost("https://internal-proxy.company.com") # your internal proxy
manager = ServerControlManager(product_id, "1.0.0", public_key,
instance_identity=identity, saved_state=saved_state)
Reverse Proxy Configuration (C++)
#include <ConnectionSettings.h>
ConnectionSettings::set_Host("https://internal-proxy.company.com"); // your internal proxy
auto manager = new ServerControlManager(productId, version, publicKey, nullptr, savedState);
Reverse Proxy Configuration (Ruby)
EvoleapLicensing.configure do |config|
config.host = "https://internal-proxy.company.com" # your internal proxy
config.timeout = 60 # seconds
end
Intermittently Connected Servers
Offline Checkout and License Files
Some SDKs let a registered server check its license out for a fixed period and later check it
back in (.NET: CheckOutLicenseForOfflineUseAsync /
CheckInOfflineLicenseAsync; Python, in preview: CheckOutLicenseForOfflineUse /
CheckInOfflineLicense). The .NET, Java and C++ managers can also register from a
license file with RegisterOfflineAsync. Checkouts are stored in the server state's
OfflineCheckouts, so persist the state afterwards.
Completely Isolated Environment
elm Lite Solution
For completely isolated environments without internet access, elm provides elm Lite - a containerized version of the backend licensing server.
elm Lite Features
- Self-Contained: No external connectivity required
- License Checkout: Licenses are "checked out" from the main portal and
installed locally (.NET and Java:
CheckOutFullLicenseAsync) - No Management Portal: Uses command-line tools for license management
- Docker-Based: Easy deployment using Docker containers
Point the SDK's host at the elm Lite server. In .NET, Java and C++, also set
ConnectionSettings' IsLocalLicenseServer to true.
To learn more about elm Lite and how to deploy it in your isolated environment, contact us at info@evoleap.com.
Best Practices
Follow these best practices to ensure reliable and secure server licensing integration.
Validation Strategy
1. Appropriate Validation Frequency
Validate every 30-60 minutes. More frequent validation adds load for little benefit; less frequent validation delays detecting expiry or a disabled instance.
2. Let the Grace Period Work
Don't add your own "ignore network errors" logic. The SDK already reports a valid result
during the validation-failure grace period and returns ServiceUnreachable only
after it runs out.
3. Startup Validation
Always validate the license during server startup before accepting requests. Don't start services if licensing fails.
Security Considerations
1. Protect State Files
Set appropriate file permissions on license state files so they cannot be edited or copied to another machine.
2. Secure Communication
Use HTTPS for the elm host or proxy, and keep certificate validation on in production.
3. Use the Right Public Key
The public key encrypts calls to elm and is shipped with your application, so it is not a secret. Each product has its own key, and staging and production differ, so make sure you ship the key for the product ID and environment you register against.
Operational Excellence
1. Comprehensive Logging
Log all licensing events including registration, validation success/failure, and shutdown events.
2. Monitoring and Alerting
Alert when validation enters the grace period, using GracePeriodExpiration, so
operators can restore connectivity before it runs out.
3. Graceful Shutdown
When shutting down due to license issues, allow time for current requests to complete before terminating.
4. Backup and Recovery
Include license state files in your backup strategy. Test license recovery procedures as part of disaster recovery planning.
Production Deployment Checklist
- License validation frequency configured (30-60 minutes)
- State persisted after every licensing call, with proper permissions
- Public key for the correct product and environment shipped with the application
- Logging configured for all license events
- Alerting set up for grace periods and validation failures
- Network connectivity to elm API verified (or proxy configured)
- Graceful shutdown procedures tested
- License state backup and recovery procedures documented