Skip to main content

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 modelMetered byOffline support
self_hostedSeats + instance activationsYes — signed token, check-ins, offline grace
saasSeats + API-call quota per periodNo — server is on the hot path

Actors​

ActorOrg typeWhat they do
Licentra Owner—Operates the platform. Super admin.
App Ownerapp_ownerPublishes products and plans. Sells directly or via resellers. Owns the product signing key.
ResellerresellerAuthorized per product. Onboards their own clients and issues licenses to them.
ClientclientThe 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 organizations table, regardless of type (app_owner, reseller, or client). 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_role enum exactly (app_owner_admin, reseller_admin, client_admin, …), except super_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.

RoleScope
super_adminEverything
app_owner_adminOwn products: plans, features, keys, policies, direct licenses, reseller authorisation, all usage of own products
app_owner_viewerRead-only version of the above
reseller_adminOwn clients + authorised products: issue / renew / suspend licenses, channel policy, approve trials
reseller_viewerRead-only version of the above
client_adminOwn tenant: assign / release seats, manage activations, view usage, manage users and API keys
client_viewerRead-only dashboards for own tenant

Permission matrix​

Actionsuperapp_owner_adminreseller_adminclient_admin
Create / edit product, plans, features✔✔ ownown 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 & quotaallown productsown clientsown tenant
Manage own org users & API keys✔✔✔✔
Audit logallown productsown clientsown 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-invites returns an inv_… 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 with POST /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, or suite:<id> for a personal account.
  • 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/start or /v1/organizations/register) made with an integration key owned by the reseller (scope license: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_id NULL — 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 own reseller_id still 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/licenses with resellerId set to the reseller's own organisation. The caller must be that organisation's reseller_admin, or the product's app_owner_admin.
  • Checks: the reseller must be authorised for the product, and the client must be a client organisation whose reseller_id is that reseller. Otherwise the call fails with 403 client_not_in_reseller_channel.
  • Result: the licence is active and 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: true and appUrl (this app's own URL, the page the admin is on) in the create call. The invitee's address is the form's inviteeEmail, 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's Origin (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.

The main way to onboard and license a client.

  1. 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-invites returns a lic_… 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: an app_owner_admin sells directly, and a reseller_admin sells as its reseller organisation (which must be authorised for the product).
  2. 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).
  3. 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_admin when the organisation has no admin yet, or client_viewer when it does, so a link can never make anyone in a tenant its admin.
  4. 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.Discover in 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 setting Config.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 gold plan. 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​

  1. The client admin requests a plan in the consuming app, which calls POST /v1/licenses/request.
  2. That creates a requested licence with reseller_id set by ChannelReseller.
  3. 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 it active.
  4. 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/start attributes the trial with the same ChannelReseller rule.
  • 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_viewer can read their own licences but not change them.

Not built​

  • Payment: there's no payment, invoicing or checkout. price_amount is display-only, and discount_pct is 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​

ConcernOwnerMechanism
License authenticityServer-held key per product; app owner sees public key and can rotateEd25519 / RSA signature over the license token
Data at restOptional per-org envelope encryption (KEK in Key Vault, wrapped DEK in DB) or cloud TDESeparate key from signing — never reuse
License key uniquenessServerCSPRNG + 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.