Licentra's own plans for vendors
Licentra is a SaaS product. Its owner (the platform owner) sells it to vendors — App Owner organizations that license their own apps through it — on Free, Starter and Professional plans. This document is the design; the status section at the end says what is built.
1. Licentra licenses itself
Licentra's plans are ordinary data in its own catalog, created in its own portal:
- a product, e.g. code
licentra(SaaS), owned by the platform owner'sapp_ownerorganization; - plans
free,starter,professionalon that product, each with the feature grants in §2; - one policy per plan (§5).
Every vendor holds a license on that product (client_org_id = the vendor's organization). services.EntitlementService reads it in-process — no HTTP call, no Gate — and the admin handlers check it before the vendor creates more.
Who is checked
| Who | Checked? |
|---|---|
Platform admin (super_admin) | Never |
| Platform owner organization (owns the self product) | Never |
Vendors (app_owner organizations) | Against their license on the self product |
| Resellers, client organizations | Never — they aren't Licentra's customers. Each reseller user takes a reseller seat on the plan of every vendor that authorizes the reseller (max_reseller_users) |
A tenant can be a reseller and a vendor at once: they are two organizations (organizations is unique per tenant and type). The vendor organization's plan, or lack of one, never affects the reseller organization. Calls made while acting as the reseller (X-Organization-Id naming it) are not metered against the vendor plan.
The deployment itself is never licensed: Licentra is the licensor, so nothing above it has to license it. That is what avoids the chicken-and-egg problem on an empty deployment.
Switching it on
| Setting | Default | Meaning |
|---|---|---|
License__SelfProductCode | empty | The self product's code. Empty = self-licensing off: every check passes, exactly as before. |
License__SelfDefaultPlanCode | free | The plan a new vendor gets at signup. |
If SelfProductCode names a product that doesn't exist, the server logs a warning and checks stay off — a typo or half-finished setup can't lock anyone out.
First-time setup
- Sign in with an address in
License__PlatformAdminEmails→super_admin. - Create the platform owner organization (
app_owner). - Create the self product (
licentra, SaaS) and its features — the codes in §2. - Create the plans and their feature grants; one of them must have the code in
SelfDefaultPlanCode. - Create the per-plan policies (§5).
- Set
License__SelfProductCodeand restart. New vendors get the default plan at signup; issue licenses to existing vendors from the normal Issue License screen.
2. Plans and feature codes
A feature missing from a vendor's license is not included (a limit of 0). A limit feature with no value is unlimited.
| Feature code | Free | Starter | Professional | Checked when |
|---|---|---|---|---|
| API requests / month (plan quota, not a feature) | 1,000 | 10,000 | 1,000,000 | Every API-key call (phase 2) |
max_products | 1 | 3 | 10 | Creating a product |
max_vendor_users | 1 | 3 | 20 | Granting a role to a new member |
max_api_keys | 1 | 1 | 10 | Creating an API key (live keys counted) |
max_reseller_users | 0 | 3 | 25 | Authorizing, inviting or reinstating a reseller, accepting a reseller invite, granting a reseller role. Counts distinct users with a reseller role in reseller organizations holding a non-revoked authorization on any of the vendor's products |
max_activations_per_license | 1 | 10 | unlimited | Creating/updating a plan on a self-hosted product |
max_active_licenses | 100 | 1,000 | 10,000 | Issuing a license (phase 2) |
node_locked | ✓ | ✓ | ✓ | Creating a self-hosted product |
key_based | ✓ | ✓ | ✓ | — (every license is key-based) |
offline_licensing | – | ✓ | ✓ | Setting a product's offline grace days above 0 |
basic_analytics | – | ✓ | ✓ | Analytics endpoints (phase 4) |
usage_metering | – | – | ✓ | Giving a plan an API-call quota |
immutable_audit | – | – | ✓ | Audit verification/export, longer retention (phase 4) |
The numbers are proposals. They are plan data, edited in the portal, not code.
Development environments were dropped. Vendors test with nfr licenses: they're excluded from activated-user counts and analytics but still count toward the API-request quota (otherwise "test" licenses would carry free production traffic). Keep them short-lived; they are signed with the product's production key.
3. Behaviour
- A refused request returns 403
{"error":"plan_limit","feature","limit","used","plan","detail"}; the portal turns it into an upgrade prompt. - Downgrades never delete anything. What already exists keeps working; the vendor just can't create more.
- A vendor without a usable plan (none, expired, revoked, suspended) is read-only: it can sign in and see everything, can't create, and its customers' check-ins and activations are never blocked.
- Signup without a default plan still succeeds (logged); the vendor is read-only until a platform admin issues a plan.
- The vendor's plan license is chosen as: usable (
active/grace), theninternal>paid>nfr>trial, then latest expiry. - Entitlements are cached per organization for one minute.
4. Activated users (end users of vendors' apps)
"Activated users" means the people using a vendor's app (seat subjects), not vendor staff in the portal.
- Activated = used the app at least once → new
license_seats.first_seen_at. - Monthly active = seen in the period →
license_seats.last_seen_at(the column exists but nothing writes it today) andseat_usage_daily. - Reporting: the shared Go client buffers the users its
Gatesees and sends them with usage calls, check-ins, or a batchedPOST /v1/licenses/{key}/seats/seen; offline installs send them at the next check-in. - Shown on a license's "Users" tab and the vendor dashboard (
basic_analytics). Pseudonymous subject ids; follow the data-retention policy. - Open: whether monthly active users becomes a billed plan limit or stays report-only.
5. API-call thresholds and rate limits live in the policy
New license_policies fields, copied onto each license like grace/overflow:
| Field | Meaning |
|---|---|
rate_limit_per_user_per_sec | Max calls per second per end user; null = unlimited |
rate_limit_burst | Short burst allowance |
quota_alert_thresholds | Percentages that notify the vendor, e.g. {80,100} — once per threshold per period |
quota_overage_pct | How far past 100% calls still pass |
quota_overage_mode | block / allow / bill (replaces the per-license hard_limit flag) |
- Policies per plan:
license_policiesgains an optionalplan_id; resolution order is reseller+plan → plan → reseller → product default. - Per-user rate limit is enforced mainly in the consuming app: the token carries
rl: {ups, burst}and the client'sGatekeeps a token bucket per user (it needs a hook that names the user). Licentra also limits per (license, subject) on calls that name a user, in memory per instance. For the self product the "user" is the vendor's API key. - Check-ins and activations are counted but never hard-blocked — blocking them would break the vendor's customers' running apps.
- Responses carry
X-Quota-Limit/Used/Remaining,X-Quota-Status: ok|warning|overage, and429+Retry-Afterwhen refused.
6. Changing a policy updates existing licenses
Policy-derived license fields become live; plan-derived ones (seats, quota, activations, features) stay a snapshot — the customer paid for those, and the plan-change action is how they move. This replaces the "frozen at issue" rule in the license lifecycle guide.
- Saving a policy bumps its version and queues a propagation job (preview: "affects N licenses").
- Affected: licenses on the policy, plus licenses that now resolve to a different policy (a new plan-specific policy, or one deleted/deactivated).
- The job runs in batches: re-apply the policy fields, bump
token_version, re-sign, audit. Progress is visible and resumes after a restart. - A grace period already running is never cut short; overflow seats already granted run out on their own schedule.
- Trial settings apply to new trials only.
- Online apps pick up the change at their next check-in (token carries
policy_version); offline installs keep the old values until they check in.