Skip to content

Policies & Features

A Policy is the template that every License is issued from. It defines the policy type, duration, optional grace period, optional activation limit, and the bag of feature flags (PolicyFeature records) that licenses derived from it will grant.

Policy Types

ts
class PolicyTypes {
  static readonly TRIAL = '000_TRIAL';
  static readonly SUBSCRIPTION = '100_SUBSCRIPTION';
  static readonly PERPETUAL = '200_PERPETUAL';
}
TypeCodeHas expirationRenewableTypical use
Trial000_TRIALYesNo (issue a fresh one)Free evaluation period
Subscription100_SUBSCRIPTIONYesYes (extend expiresAt)Paid time-bound license
Perpetual200_PERPETUALNo (duration is null)NoOne-time grant, no expiry

The PolicyTypes value is purely a label — LicenseManagementService.renew() rejects any license whose policy has no duration (i.e. perpetual) regardless of the type code.

Policy Configuration

Duration

Controls how long an issued license stays valid.

ts
interface IDuration {
  unit: TDurationUnit;  // 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'
  value: number;
}

DurationMultipliers.toMilliseconds(duration) converts an IDuration to a millisecond count, used by issue() and renew() to compute expiresAt.

UnitConstantMilliseconds
millisecondMILLISECOND1
secondSECOND1,000
minuteMINUTE60,000
hourHOUR3,600,000
dayDAY86,400,000
weekWEEK604,800,000
monthMONTH30 days = 2,592,000,000
yearYEAR365 days = 31,536,000,000

Calendar-naive

month is hard-coded to 30 days and year to 365 days. There is no calendar-aware arithmetic, no leap-year handling, and no DST handling. If finance or billing later need calendar precision, that has to be added separately.

A perpetual policy stores duration: null. issue() then leaves both expiresAt and graceExpiresAt as null.

Grace Period

Optional extension after expiresAt. While now < graceExpiresAt, validation still returns valid: true but with code: GRACE_PERIOD instead of VALID.

json
{
  "duration":    { "unit": "year", "value": 1 },
  "gracePeriod": { "unit": "day",  "value": 7 }
}

A license issued today against the above policy:

PhaseDurationValidation result
Activedays 0–365valid: true, code: VALID
Gracedays 365–372valid: true, code: GRACE_PERIOD
Expiredday 372+valid: false, code: LICENSE_EXPIRED (status auto-flips to expired on next validation)

gracePeriod is itself an IDuration. If omitted, the license simply transitions from active straight to expired.

Activation Config

Caps the number of unique device fingerprints that may activate against one license.

ts
interface IActivationConfig {
  limit: number;
}
FieldBehavior
limitMax unique (licenseId, fingerprint) rows in the Activation table

No "fixed vs floating" modes

There is no mode field on IActivationConfig. There is no floating activation, no heartbeat mechanism, and no automatic slot reaping. An activation slot is held until somebody explicitly deletes the activation row (DELETE /activations/{id}) or the parent license is deleted (which cascades).

If policy.activation is null (or absent), activations are unlimited.

Per-license override

Individual licenses can override the policy's activation config and feature values via the override JSONB column on License:

json
{
  "override": {
    "activation": { "limit": 10 },
    "features": {
      "max_products": 1000,
      "custom_branding": true
    }
  }
}
  • LicensingBaseService.resolveActivation() returns license.override.activation ?? policy.activation ?? null.
  • LicensingBaseService.resolveFeatures() merges policy features → override features, with override values taking precedence.

Override updates don't republish

The override field is set via PATCH /licenses/{id}, which is a plain CRUD update — it does not republish the cached certificate. To force the new override into Redis (and therefore into LicenseMiddleware consumers), you currently need to trigger a state-changing lifecycle operation such as suspend → reinstate or renew. The POST /validation/validate endpoint always reads fresh from the database, so it reflects the change immediately.

Feature Flags

PolicyFeature rows are typed key/value flags attached to a policy.

Statuses

ts
class PolicyFeatureStatuses {
  static readonly ACTIVATED = Statuses.ACTIVATED;     // 'activated'
  static readonly DEACTIVATED = Statuses.DEACTIVATED; // 'deactivated'
}

When a feature is deactivated, LicensingBaseService.resolveFeatureValue() returns the DATA_TYPE_DEFAULTS falsy value for its type instead of the configured value:

dataTypeActive value sourceDeactivated value
BOOLEANboValue ?? truefalse
NUMBERnValue ?? 00
TEXTtValue ?? ''''
JSONjValue ?? nullnull

This lets you keep a feature row in place but turn it off without deletion.

Data Types

dataTypeColumnTypeScriptExample
BOOLEANboValue (boolean)booleantrue
NUMBERnValue (numeric)number100
TEXTtValue (text)string"professional"
JSONjValue (jsonb)Record<string, any>{ "modules": ["pos","crm"] }

The dataType value comes from the shared DataTypes constants in @venizia/ignis-helpers.

Adding features

There is no dedicated PolicyService.addFeature() method — features are managed via the standard PolicyFeatureController CRUD endpoints:

http
POST /v1/api/licensing/policy-features
json
{
  "policyId": "<policy-id>",
  "code": "max_products",
  "name":        { "en": "Maximum Products", "vi": "Sản phẩm tối đa" },
  "description": { "en": "Max products allowed", "vi": "Số sản phẩm tối đa" },
  "dataType": "NUMBER",
  "nValue": 500,
  "status": "activated",
  "sequence": 10
}

The (policyId, code) pair is unique. Deleting a policy cascades and removes all its features.

Feature Resolution

LicensingBaseService.resolveFeatures({ policyId, featuresOverride }):

  1. policyFeatureRepository.find({ where: { policyId } }) — loads all features (both activated and deactivated)
  2. For each feature, calls resolveFeatureValue():
    • If status !== activated, return the DATA_TYPE_DEFAULTS value for its type
    • Otherwise, return the matching <x>Value column (with the per-type fallback above)
  3. Build a Record<string, unknown> keyed by feature.code
  4. If featuresOverride is provided (from license.override.features), spread it on top so overrides win

The resulting record is what shows up in the validation response under features, and inside the certificate payload's features field.

/policies/catalogs

Public catalog endpoint exposed by PolicyController. It loads every Policy whose status is ACTIVATED, ordered by sequence ASC, with each policy's ACTIVATED features inlined (also ordered by sequence ASC):

http
GET /v1/api/licensing/policies/catalogs
json
{
  "data": [
    {
      "id": "...",
      "name": "BANA Professional — Yearly",
      "type": "100_SUBSCRIPTION",
      "duration":    { "unit": "year", "value": 1 },
      "gracePeriod": { "unit": "day",  "value": 14 },
      "activation":  { "limit": 5 },
      "features": [
        { "code": "max_products",   "dataType": "NUMBER",  "nValue": 500 },
        { "code": "custom_branding","dataType": "BOOLEAN", "boValue": true }
      ]
    }
  ]
}

The route is still gated by the Policy.find permission — the controller does not bypass authentication.

Data Model

Full column reference, indexes, and constraints for Policy and PolicyFeature live in Domain Model. Key points for this page: Policy is soft-deletable; PolicyFeature is not (deleted column disabled) and cascade-deletes with its parent Policy; (policyId, code) is unique; the feature value lives in the typed column matching dataType (boValue/nValue/tValue/jValue).

PageDescription
Domain ModelPolicy + PolicyFeature full schema
License LifecycleHow licenses move through states
Validation & ActivationHow features and activations are resolved at validation time
ConfigurationSeeded FREE_TRIAL policy

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.