License technical overview
When you buy a user plan, you are buying a set of licenses. Most of these licenses are hidden from the users view - they just pick a user-plan and go. The code cannot deal with user plans - they change from version to version, and are re-packaged whenever mercury is in retrograde.
So the code deals with module licenses, not user-plans. Module license names are static and kept in SoLicenseNames.
The price list will change - so we want to avoid hard-coding references to it into the application. Instead of checking for a user plan identifier like "Sales-PREMIUM", we want to check for a feature identifier like "SALE-CAL", and have the license server handle the mapping between the price list and the features.
If the price list changes, we don't have to update all the checks - just update the license server.
Visible vs hidden licenses
The user sees a set of licenses in the admin panel, but the license server delivers a set of hidden licenses containing feature-specific licenses.
License types
The ModuleLicense.Type field determines how a license is scoped and assigned:
| License type | ModuleLicense.Type | Description |
|---|---|---|
| System licenses | 1 | Define which features are available system-wide. Example: The saint license is present if Sales Intelligence is enabled. This license is hidden (not on the price list) and implicitly activated. The SuperOffice client checks for it and enables SAINT features if present. |
| Site licenses | 2 | Rarely used today. Historically used in satellite setups, where certain licenses were assigned to specific sites instead of being globally available. |
| User licenses | 3 | Licenses assigned directly to users. The number of assigned users cannot exceed the number of available licenses. Some user licenses may be hidden to simplify the UI. These are activated through user plans. User plans have ModuleLicense.ExtraFlags = 1 and define implied licenses via the ExtraInfo field, for example:"set=user,web,chat-cal" assigns the user, web, and chat-cal licenses automatically. |
CALs
In the olden days we sold CALs (Client Access Licenses) directly. To use the web client, you would buy a "user" CAL to login, a "web" CAL to use the web client, and then an "remote-travel" CAL to use another module. Administrators would tick off the boxes per user, and the feature would be activated for that user.
These days we follow the same principle. Feature licenses are split into two parts:
- feature: a system license to indicate that the feature is present and activated. Often a user without a CAL should still be able to view the feature in read-only mode.
- feature-cal: a user license to indicate that the user has edit rights on the feature.
The navigator will check if the feature system license is present.
The edit button will check if the feature-cal user license is on. If the user doesn't have it, the edit button will be disabled.
The admin client will check if the feature system license is present, and hide the corresponding admin page if missing.
Sub-features
Sometimes an existing feature will be split into pieces in order for parts to be sold as part of a premium package.
For example:
- selection: system license that controls the selection panel visibility.
- selection-cal: user license that controls selection edit button.
- selection-combined-cal: user license that controls access to creating combined selections. The "combined-cal" license was added later to.
Similarly new licenses for "escalate" , "inbox-filter", and "request-batch" were added to help differentiate the essential and premium layers of service user plans. The code checks for "escalate", not the user plan, so that we can easily change the licenses in a particular user plan without updating the client.
Checking for licenses
The LicenseAgent.GetUserLicenses Agent API and the REST API /api/v1/License/ownername/modulename support checking license status.
The /api/v1/License/ownername/modulename endpoint will tell you if the license exists or not, but will not tell you if the license is assigned to the user. It is therefore mostly useful for checking for global system licenses like sale, project or quote, not user-specific licenses like sale-cal or quote-cal.
The current user principal will give you a list of the current user's assigned licenses:
GET User/currentPrincipal will return a list of licenses the current user has.
{
"UserType": "InternalAssociate",
"Associate": "HugoBoss",
"AssociateId": 3456,
"IsPerson": true,
"PersonId": 321,
"CountryId": 578,
"HomeCountryId": 578,
"ContactId": 3,
"GroupId": 10,
"ContactOwner": 1234,
"RoleId": 50331659,
"RoleName": "Standard",
"RoleType": "Employee",
"Licenses": [
{
"OwnerName": "SUPEROFFICE",
"OwnerDescription": "SuperOffice AS",
"Name": "server",
"Description": "SuperOffice Server",
"Version": "11.11",
"LicenseType": "SiteLicense",
"ExtraFlags": 0,
"ExtraInfo": "",
"LicenseNumber": 1,
"IsHidden": true,
"IsUnrestricted": false,
"ExpiryDate": "2026-06-11T00:00:00",
},
{
"OwnerName": "SUPEROFFICE",
"OwnerDescription": "SuperOffice AS",
"Name": "guide-cal",
"Description": "Guides",
"Version": "11.11",
"LicenseType": "UserLicense",
"ExtraFlags": 0,
"ExtraInfo": "",
"LicenseNumber": 1600,
"IsHidden": true,
"IsUnrestricted": false,
"ExpiryDate": "2026-06-11T00:00:00",
},
{
"OwnerName": "SUPEROFFICE",
"OwnerDescription": "SuperOffice AS",
"Name": "ten-salesservicemarketing",
"Description": "SalesPremiumServicePremiumMarketingPremium",
"Version": "11.11",
"LicenseType": "UserLicense",
"ExtraFlags": 1,
"ExtraInfo": "set=user,web,pocket-crm-cal,selection-cal,relation-cal,report-cal,project-cal,guide-cal,saint-cal,selection-combined-cal,mail-merge-cal,chat-cal,forms-cal,ej-client,t2,dash-cal,sale-cal,target-cal,quote-cal,stakeholder-cal,ej-mod-spm-cal,mktg-auto-cal",
"LicenseNumber": 700,
"IsHidden": false,
"IsUnrestricted": false,
"ExpiryDate": "2026-06-11T00:00:00",
},
...
So from this we can see we have a superoffice.server site license, and the current user has been assigned a superoffice.guide-cal user license, courtesy of their superoffice.ten-salesservicemarketing user-plan license.
Note
User licenses that are not assigned to the user do not appear in the payload. So if you want to know if the license exists, use the /api/v1/License/ownername/modulename endpoint to check.
The currentPrincipal also has useful information like the role function-rights.
Counting users
There are two approaches:
1: Get the license and read the number of user or web licenses
Users must have both user and web to log in to the SuperOffice web application. This number is the upper bound. It does not tell you how many are in use.
For some customers, the number of licenses is huge, because they are paying by use, using SCIM. To handle this, count the number of user licenses in use, rather than the total number of licenses available.
2: Get the license and sum the number of ExtraFlags=1 licenses in use
User plans are what the user is paying for. They define multiple implied, hidden licenses.
The same SCIM caveat applies: count the number of user plans in use, rather than the total number available.
License signing
Licenses are signed using public/private keys.
The private key is a closely guarded secret and without it, you cannot make a keycode generator.
Individual moduleLicense rows are also signed and all rows are also hash-checked to make tampering harder.
Summary: You touch them, they stop working. SoAdmin and NetServer can edit them, no one else. Hackers can hack the DLLs, but not make a keycode generator that works with un-hacked code.