Overview & roles
Purpose
The Licentra issues, validates and meters software licenses for third-party applications. It supports two licensing models and a multi-party sales chain (app owner → optional reseller → client).
| Licensing model | Metered by | Offline support |
|---|---|---|
self_hosted | Seats + instance activations | Yes — signed token, check-ins, offline grace |
saas | Seats + API-call quota per period | No — server is on the hot path |
Actors
| Actor | Org type | What they do |
|---|---|---|
| Licentra Owner | — | Operates the platform. Super admin. |
| App Owner | app_owner | Publishes products and plans. Sells directly or via resellers. Owns the product signing key. |
| Reseller | reseller | Authorized per product. Onboards their own clients and issues licenses to them. |
| Client | client | The tenant that holds licenses. Assigns seats to its end users. |
| Consuming app | — | Machine identity (API key) that validates licenses, checks in, reports usage. |
A client organisation optionally carries reseller_id. A reseller sees only clients where reseller_id = self. An app owner sees every license of its products regardless of channel.
Terminology
Two words are easy to conflate across these docs — pin them down once, here, and treat every other doc's usage as deferring to this definition:
- "Tenant" always means a Client organisation (
organizations.type = 'client') — the org that holds licenses and whose end users consume a product. It never refers to an App Owner's or Reseller's own organisation account. "An app owner sees N tenants" = N client orgs use that app owner's product(s); it does not mean the app owner has multiple accounts of its own. - "Organisation" / "org" is the generic term for any row in the
organizationstable, regardless oftype(app_owner,reseller, orclient). An App Owner or Reseller each belong to exactly one organisation of their own — call that "their organisation," never "their tenant." - Role names in prose match the
user_roleenum exactly (app_owner_admin,reseller_admin,client_admin, …), exceptsuper_admin, which is also called the Licentra Owner — the two names are synonyms, not different roles.
Roles
Roles are granted per (user, organisation). One user may hold roles in several organisations. super_admin has no organisation.
| Role | Scope |
|---|---|
super_admin | Everything |
app_owner_admin | Own products: plans, features, keys, policies, direct licenses, reseller authorisation, all usage of own products |
app_owner_viewer | Read-only version of the above |
reseller_admin | Own clients + authorised products: issue / renew / suspend licenses, channel policy, approve trials |
reseller_viewer | Read-only version of the above |
client_admin | Own tenant: assign / release seats, manage activations, view usage, manage users and API keys |
client_viewer | Read-only dashboards for own tenant |
Permission matrix
| Action | super | app_owner_admin | reseller_admin | client_admin |
|---|---|---|---|---|
| Create / edit product, plans, features | ✔ | ✔ own | own plans on authorised products | – |
| Create client license links | ✔ | ✔ own products (direct) | ✔ authorised products, own channel | – |
| Rotate product signing key | ✔ | ✔ own | – | – |
| Authorise reseller & set ceilings | ✔ | ✔ own | – | – |
| Define trial / grace / overflow policy | ✔ | ✔ own (direct) | ✔ own channel, within ceilings | – |
| Create organisation directly (any type, incl. client) | ✔ | – | – | – |
| Issue license key | ✔ | ✔ own products | ✔ authorised products, own clients | – |
| Approve trial request | ✔ | ✔ | ✔ own channel | – |
| Renew / extend / upgrade plan | ✔ | ✔ | ✔ own clients | – |
| Suspend / revoke | ✔ | ✔ | ✔ own clients, own-issued only | – |
| Override features / limits on a license | ✔ | ✔ | ✔ within ceilings | – |
| Assign / release seats | ✔ | ✔ | ✔ | ✔ own tenant |
| Deactivate self-hosted instance | ✔ | ✔ | ✔ | ✔ own tenant |
| View usage & quota | all | own products | own clients | own tenant |
| Manage own org users & API keys | ✔ | ✔ | ✔ | ✔ |
| Audit log | all | own products | own clients | own tenant |
| Platform settings | ✔ | – | – | – |
Rules:
- A reseller cannot revoke a license the app owner issued directly; the app owner can always revoke any license of its product.
- Reseller seat visibility is optional — drop it for a stricter privacy boundary between reseller and client end users.
Who creates a license key
The App Owner for direct sales, or the Reseller for products it is authorised to sell, on behalf of its own clients. The server generates the key; humans never choose it. licenses.reseller_id records which path issued it. Client admins consume and assign; they never issue.
Selling through a reseller
How the reseller channel works in the current code.
1. The app owner authorises a reseller
An app_owner_admin does this from the product's Resellers tab. Each authorisation is one reseller_products row per (reseller, product). It carries discount_pct and the ceilings max_trial_days, max_trial_seats, max_expiry_grace_days, max_overflow_days and max_overflow_seats (NULL = no ceiling). There are three ways to create it:
- Invite link (the UI's main path):
- Create:
POST /v1/admin/products/{id}/reseller-invitesreturns aninv_…link once. Only its SHA-256 hash and a short prefix are stored. It expires after 14 days by default. Optionally it also emails the link (see Invitation email below). - Accept: the reseller opens
/accept-invite/{token}, signs in, and accepts withPOST /v1/admin/reseller-invites/{token}/accept. This needs no role. - Result: it finds or creates a reseller organisation keyed to the caller's own organisation, authorises it with the invite's terms, and grants
reseller_admin. The key is the work tenant id, orsuite:<id>for a personal account.
- Create:
- Invite by tenant id:
POST /v1/admin/products/{id}/resellers/invite, where the app owner types the reseller's tenant id. - Direct authorise:
POST /v1/admin/products/{id}/resellers, for an existing reseller organisation.
The UI sets only the discount; the ceilings are API-only today.
2. A client joins the reseller's channel
- How the link is set: a client organisation belongs to a reseller through
organizations.reseller_id. It's set when the client org is created by a self-serve call (/v1/licenses/request,/v1/trials/startor/v1/organizations/register) made with an integration key owned by the reseller (scopelicense:request), or when the client accepts the reseller's client license link (§4a). - Without a reseller key: a client first seen through the app owner's own key is a direct client.
- Adopted once: a direct client (
reseller_idNULL — typically one the consuming app auto-registered before it bought anything) joins a reseller's channel the first time it buys through that reseller, by link or through the reseller's key (OrganizationRepo.AdoptIntoChannel). A client already in another reseller's channel stays there; the licence's ownreseller_idstill records the sale. - No other moves: there is no move between resellers or back to direct.
3. Which reseller a sale belongs to
LicenseService.ChannelReseller decides. It's the client's reseller_id, as long as that reseller's authorisation for the product isn't revoked; otherwise it's a direct sale (NULL). Everything below uses it.
4. The reseller issues a licence
- Endpoint:
POST /v1/admin/licenseswithresellerIdset to the reseller's own organisation. The caller must be that organisation'sreseller_admin, or the product'sapp_owner_admin. - Checks: the reseller must be authorised for the product, and the client must be a client organisation whose
reseller_idis that reseller. Otherwise the call fails with403 client_not_in_reseller_channel. - Result: the licence is
activeand signed immediately, with no app-owner approval. - Plans and policy: the reseller sells the app owner's plans or its own (§4b). The policy is the reseller's channel policy if it has one, otherwise the app owner's direct-sale policy.
Invitation email (optional)
Both invite forms (reseller invites and client license links) can email the link instead of leaving the seller to copy it.
- Request:
sendEmail: trueandappUrl(this app's own URL, the page the admin is on) in the create call. The invitee's address is the form'sinviteeEmail, which is required for an email. - Sender: the suite's mail service, from
License__MailFrom, with an app-only token. It is not the signed-in admin's own mailbox, and no per-user mail permission is involved. The same path sends checkout email. - Reply-To:
License__MailReplyTo, this deployment's configured address (the same for every invite). The invitee's reply goes there, not to the inviting admin. - Link: built on
appUrl, which the API accepts only when it is on the request'sOrigin(the same check the rest of the suite applies to browser-supplied URLs). - Result: the create call returns the invite with
emailSent(true or false) and, on failure,emailError. Errors:mail_not_configured(no checkout mailbox set, or no sender wired),mail_invalid_message(bad invitee address or app URL),mail_failed(the mail service send failed). A mail error never fails the invite; the link is still returned to copy. - Scope: invite emails are sent only from the admin's explicit create request, never from a background job or
/internal/*route.
4a. Client license links (no manual input)
The main way to onboard and license a client.
- Create: the reseller (or the app owner, for a direct sale) opens the product's Client links tab and picks a plan and kind (paid or trial).
POST /v1/admin/products/{id}/client-invitesreturns alic_…link once. Only its hash is stored. Links are single use and expire after 14 days by default. Who may create one follows the plan-writer rule: anapp_owner_adminsells directly, and areseller_adminsells as its reseller organisation (which must be authorised for the product). - Share: the seller sends the link (
/accept-license/{token}) to the client, by copying it or by ticking Email this link on the form (see Invitation email below). - Accept: the client opens it, signs in with a work or personal account, sees the offer (
GET /v1/admin/client-invites/{token}) and clicks Accept (POST …/accept). In one step the server:- registers the client organisation, keyed to the caller's organisation (work tenant id, or
suite:<id>), in the seller's channel; - issues the licence (
LicenseService.Issue) with the link's plan, kind and reseller; - grants the accepter
client_adminwhen the organisation has no admin yet, orclient_viewerwhen it does, so a link can never make anyone in a tenant its admin.
- registers the client organisation, keyed to the caller's organisation (work tenant id, or
- Activate: the consuming app finds the licence by itself. Its licence gate sees the tenant has no usable licence, lists the tenant's licences (
GET /v1/licenses/mine), adopts the best usable one and activates it (license.Client.Discoverin the shared Go client). This happens at most once a minute per tenant. A third-party Go app gets the same behaviour from that module by settingConfig.TenantForSubject.
Everything that can be checked without side effects (link usable, plan sellable, reseller still authorised, no active licence for the same plan) is checked before the link is claimed, and a failed issue reopens it, so a refusal never uses the link up.
4b. Reseller plans
A reseller can publish its own plans on a product it's authorised for, next to the app owner's: product_plans.reseller_id set (migration 0003).
- Who sees them: that reseller and the app owner, and nobody else. Other resellers, the App Store and direct self-serve pickers only ever see the app owner's plans.
- Who edits them: only that reseller. The app owner reads them but can't edit them.
- Who sells them: only that reseller (
planSellableBy, checked on issue, request, renewal and trial conversion). - Codes: unique within one owner of a product, so two resellers can both have a
goldplan. A reseller's integration key resolves its own plan code first, then the app owner's. - Limits: none beyond the product's features. Seats, quota and duration are the reseller's choice, and the app owner's Licentra plan checks (
usage_metering, activations) still apply.
5. The client buys from inside the app
- The client admin requests a plan in the consuming app, which calls
POST /v1/licenses/request. - That creates a
requestedlicence withreseller_idset byChannelReseller. - The client's reseller (or the app owner) sees it in their licence list and approves it with
POST /v1/admin/licenses/{key}/approve. Approval signs the licence under the channel's policy and makes itactive. - The app picks up the key by itself on the tenant's next request (licence discovery, §4a step 4). The app's own licence screen can still install it with
PUT /api/admin/license.
Requests created before this attribution existed have reseller_id NULL, so only the app owner can approve those.
6. Trials
- Attribution:
POST /v1/trials/startattributes the trial with the sameChannelResellerrule. - Self-serve policy (
trial_self_serve): the key is issued at once and the app installs it. - Otherwise: the trial stays pending until the channel's reseller (or the app owner) approves it with
POST /v1/admin/trials/{id}/approve.
7. After the sale
- Who can act: the reseller can renew, suspend, resume and revoke licences where
reseller_id = self. The app owner can do this on every licence of its product. - What the client sees: a
client_admin/client_viewercan read their own licences but not change them.
Not built
- Payment: there's no payment, invoicing or checkout.
price_amountis display-only, anddiscount_pctis stored but never applied. - Reseller UI: resellers have no UI for channel policies or ceilings.
- Upgrades by link: a client that already holds a valid licence keeps using it in the app. Discovery only adopts a licence when the current one isn't usable, so a second link for a better plan needs its key installed by hand, or the old licence revoked.
Key-material design
| Concern | Owner | Mechanism |
|---|---|---|
| License authenticity | Server-held key per product; app owner sees public key and can rotate | Ed25519 / RSA signature over the license token |
| Data at rest | Optional per-org envelope encryption (KEK in Key Vault, wrapped DEK in DB) or cloud TDE | Separate key from signing — never reuse |
| License key uniqueness | Server | CSPRNG + UNIQUE constraint; key is a lookup handle, not a secret |
Resellers never hold keys. A reseller issues a license by calling the server; the server signs with the product owner's key, so the consuming app needs only one public key per product.
Scoping in the database
Every scoped table carries client_org_id, reseller_id or product_id. Row-level security can enforce the matrix with one policy per table using app.role / app.org_id session settings. RLS is not enabled today; scoping is enforced in the application's API handlers.