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 UserControlManager as described in the Web Applications guide, or (preview, API v3.5) check a user without a session using validate_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_user Preview (API v3.5, not yet released): product ID, instance ID, user ID and version. See Per-User Checks.

Simple Control Flow

1. Load State
Restore the persisted server state (none on first run)
→
2. Register
Only if the state is not registered
→
3. Validate Instance
Start serving if valid; save state
→
4. Periodic Validation
Re-validate on a timer; save state
→
5. Handle Changes
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: IsValid is true. Keep serving.
  • elm unreachable, within grace: IsValid is true and IsInValidationFailureGracePeriod is true; GracePeriodExpiration says 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_data from validate_user, and ucm.session_state.custom_data after a user session. ServerState does not store custom data, so validate_instance does not expose it.
  • C#, Java, Python, C++: custom data is parsed from user and app session responses (for example IValidatedSessionState.CustomData in .NET), not from ServerControlManager validation. The .NET IServerState carries a CustomData property through IControlState, 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