Skip to content

PRD: License lifecycle, activation & runtime validation

ModuleLicensing (CORE-15)PRD IDPRD-ACT-001
StatusShippedOwnerLicensing squad
Date2026-06-15Versionv1.0
Packages@nx/licensing · @nx/coreURDLIC · ACT · VAL

TL;DR

Turns an issued license into something a running product can trust. A license is issued from a plan with a unique key, then driven through a guarded lifecycle - suspend, reinstate, renew, revoke - where each transition is checked against the current status, audited, and triggers a re-signed certificate. Devices activate against the license by fingerprint, each unique device taking one seat up to the plan's limit. At runtime any service validates a key and gets back a status code, the resolved feature set, the seat usage, and a signed certificate it verifies offline - so feature and seat gating never needs a live call to licensing. Lifecycle transitions are refused when they don't make sense (renew a perpetual, reinstate a revoked), and expiry is detected lazily on the next validation rather than swept eagerly.

1. Context & Problem

PRD-PLN-001 established the licensing spine - reusable plans with typed feature flags, license issuance, and a first validation call. It defines what a license is; it does not specify, in depth, the operational runtime path a license travels once issued: how each lifecycle transition is guarded, how devices take and release seats, and exactly what a validation call returns under every license state.

Without that depth, the runtime contract is ambiguous. Can an expired license be renewed back to life? What happens when a suspended license is renewed? Does a device that re-validates consume a second seat? What code does a service receive when a license is past its grace window versus merely suspended? Each consuming service would have to guess, and the answers would drift apart across modules.

This increment pins down the issue → activate → validate path as one trustworthy, code-accurate contract: a status-guarded lifecycle state machine, fingerprint-based device activation with seat enforcement, and a validation pipeline whose outcome codes, lazy-expiry behaviour, resolved features, seat handling, and signed certificate are all specified - so every service gates features and seats offline against one definition.

2. Goals & Non-Goals

Goals

  • Issue a license from a plan with a unique generated key, computing expiry and grace from the plan.
  • A guarded lifecycle - suspend, reinstate, renew, revoke - where every transition validates the current status, is audited, and re-signs the certificate.
  • Device activation by fingerprint that is idempotent per device and enforces the effective seat limit.
  • A runtime validation call returning a precise status code, the resolved feature set, the seat usage, and a signed certificate.
  • Lazy expiry - a license past its grace window flips to expired on the next validation, with no background sweeper.
  • A signed certificate consuming services verify offline, refreshed on every lifecycle change.

Non-Goals

  • Defining plans and feature flags themselves - specified in PRD-PLN-001 (URD-PLN).
  • Billing, invoicing, dunning, or payment-charge integration.
  • Per-feature runtime enforcement inside consuming modules - each module gates itself against the certificate.
  • Usage metering, heartbeats, or eager / scheduled expiry sweeping.
  • A license-management UI (frontend concern).

3. Success Metrics

MetricTarget / signal
Lifecycle correctnessEvery guarded transition (renew a perpetual, reinstate a non-suspended, renew/reinstate a revoked) is refused with a status-specific reason
Seat integrityOne seat per unique (license, device fingerprint); a re-activating device never consumes a second seat
Validation determinismValidation outcome code matches the license state for every case (valid / grace / not-started / expired / suspended / revoked / not-found / seat-limit)
Offline gatingConsuming services gate features and seats against the certificate - zero live licensing calls per request
Audit completeness100% of lifecycle and activation actions write an append-only event; each lifecycle change re-signs the certificate

4. Personas & Use Cases

PersonaGoal in this feature
Platform OperatorIssue a license and drive it through suspend / reinstate / renew / revoke with confidence each transition is guarded
Merchant OwnerSelf-serve a single free trial and run on a valid license across their devices
Device / POS clientActivate against a license by fingerprint and stay within the seat limit
Consuming serviceValidate a key, read the resolved features and seat usage, and gate offline against the signed certificate

Core scenario: an operator issues a subscription license for a merchant; the merchant's POS devices activate by fingerprint, each taking one seat up to the plan's limit; on every launch a service validates the key and receives VALID with the resolved features, seat usage, and a signed certificate it caches and verifies offline. When the license passes its grace window, the next validation flips it to expired and returns LICENSE_EXPIRED; renewing it restores it to active with a fresh validity window and re-signs the certificate.

5. User Stories

  • As a platform operator, I issue a license from a plan and get a unique key, so the entitlement is tied to a known principal.
  • As a platform operator, I suspend then reinstate a license, and the system refuses to reinstate anything that isn't suspended, so the lifecycle can't be corrupted.
  • As a platform operator, I renew a license to extend it, and renewing a perpetual or a revoked license is refused, so renewal only happens where it makes sense.
  • As a platform operator, I revoke a license permanently, knowing revoked is terminal and can never be renewed or reinstated.
  • As a device client, I activate by fingerprint and re-activating the same device reuses its seat, so a device never burns two seats.
  • As a consuming service, I validate a key once and gate features and seats offline against the certificate, so I never call licensing per request.
  • As a consuming service, I get a precise status code on validation, so I can tell a grace-period license from a suspended one.

6. Functional Requirements

#RequirementURD ref
FR-1Issue a license from a plan with a unique generated key; compute expiry and grace from the plan's duration (perpetual = no expiry)URD-LIC-001..002
FR-2Suspend an active license, then reinstate it - reinstate is refused unless the current status is suspendedURD-LIC-003..004
FR-3Renew extends validity from the later of the current expiry or now and restores an expired license to activeURD-LIC-005
FR-4Renew is refused for a perpetual license, and for a suspended or revoked licenseURD-LIC-006..007
FR-5Revoke is terminal - a revoked license can never be renewed or reinstatedURD-LIC-007
FR-6Every lifecycle action writes an append-only event and re-signs & re-publishes the certificateURD-LIC-009..010
FR-7A merchant self-serves a single free-trial license; an existing trial is returned, never duplicatedURD-LIC-008
FR-8Activate a license on a device by fingerprint (optional label / platform / hostname); re-activating the same device reuses its seatURD-ACT-001..002
FR-9Active device seats cannot exceed the effective seat limit (override else plan); a null limit means unlimited; deactivation frees a seatURD-ACT-003..005
FR-10Activation is refused when the license is not in an active statusURD-ACT-007
FR-11Validate a key and return a result code: valid, grace-period, not-started, expired, suspended, revoked, not-found, or seat-limit-reachedURD-VAL-001..004 · URD-VAL-007
FR-12A license past its grace window is lazily flipped to expired on validation - no background sweeperURD-VAL-005
FR-13Validation returns the resolved feature set (per-license overrides applied) and the seat usage (used / limit)URD-VAL-006..007
FR-14A successful validation returns a signed certificate other services verify offline; validation records the last-validated time best-effortURD-VAL-008..009

Full requirement text and acceptance criteria live in the Licensing URD. This PRD references them rather than restating them. Plans and feature-flag requirements (URD-PLN) are specified in PRD-PLN-001.

7. Non-Functional Requirements

AreaRequirement
ConsistencyLifecycle transitions lock the license row and run in a transaction; a partial failure rolls back fully
ConcurrencyLazy expiry uses a conditional status flip so two simultaneous validations cannot double-transition; a losing writer re-reads the actual status
IdempotencyA device seat is keyed on (license, fingerprint) - re-activation and re-validation of the same device never create a second seat
Data integrityNo state change without a matching append-only audit event; each lifecycle change re-signs the certificate
TrustThe certificate is Ed25519-signed and verifiable offline by any consuming service; it is cached with a TTL and refreshed on lifecycle change
Performance / scaleConsumers verify the certificate offline - no live licensing call per request; expiry is detected lazily, never swept
Tenancy & authzA license is bound to exactly one principal (merchant or user); lifecycle and activation actions are gated by licensing-scoped permissions
i18nLicense display name is bilingual ({ en, vi })

8. UX & Flows

License lifecycle state machine - each transition is guarded by the current status:

Runtime validation with device seat handling:

Backend-only surface in @nx/licensing - issuance and lifecycle actions, device activation, and the validation call. There is no license-management UI in this increment.

9. Data & Domain

EntityRole in this path
LicenseThe issued entitlement - unique key, status, computed expiry / grace, optional overrides, and the cached signed certificate
ActivationOne device seat bound to a license by fingerprint, with optional label / platform / hostname
LicenseEventAn append-only audit record of every lifecycle and activation action
PolicyThe plan a license is issued from - supplies duration, grace, seat limit, and feature flags read during resolution
PolicyFeatureA typed feature flag resolved into the validation result and the certificate

Conceptual only - full schema and invariants in the licensing domain model. All five tables live in @nx/core and are re-exported by @nx/licensing.

10. Dependencies & Assumptions

Depends on

  • Plans & feature flags (PRD-PLN-001, URD-PLN) - a license is issued from a plan whose duration, grace, seat limit, and flags drive this path.
  • Commerce (Commerce) - a license is bound to a merchant (or user) principal.
  • Permissions (Permissions) - lifecycle and activation actions are gated by licensing-scoped permissions.
  • A signing key pair (Ed25519) for the certificate, with the public key distributed to consumers for offline verification.

Assumptions

  • A free-trial plan is seeded so merchants can self-serve from day one.
  • Consuming modules do their own per-feature gating against the certificate; licensing does not enforce features inside them.
  • Reporting tolerates lazy (not eager) expiry detection.

11. Risks & Open Questions

Risk / questionMitigation / status
Two concurrent validations both detect expiryConditional status flip - only one writer transitions; the other re-reads the actual current status
A device re-validating could consume a second seatSeat is keyed on (license, fingerprint) - an existing seat is reused before any new one is created
Lazy expiry may lag reporting accuracyAccepted; expiry flips on the next validation. Open: a scheduled sweeper if eager expiry is ever needed
Lifecycle transition on a stale readEach transition locks the license row inside a transaction before checking status
Certificate trust depends on key distributionPublic key shipped to consumers; the rotation path must be defined before any key change
Fixed-duration arithmetic for renew / expiryOpen: move to calendar-aware months / years if required

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 (lifecycle + validation) and P2 (device activation + certificate distribution) - see URD feature catalog
RolloutPart of @nx/licensing; no feature flag; a free-trial plan is seeded so merchants self-serve from day one
MigrationNone - runs on the existing licensing entities (tables live in @nx/core)
Launch criteriaIssue → activate → validate works end-to-end; every guarded transition is refused with a status-specific reason; a device never burns two seats; validation returns the correct code, resolved features, seat usage, and a certificate verified offline; a past-grace license flips to expired on the next validation
MonitoringValidation outcome-code distribution, seat-limit rejection rate, lifecycle-action vs. audit-event consistency, certificate signing failures

13. FAQ

Can I renew an expired license back to life? Yes - renew restores an expired license to active with a fresh validity window. But renew is refused for a perpetual license (nothing to extend) and for a suspended or revoked one.

Is revoke reversible? No - revoked is terminal. A revoked license can never be renewed or reinstated; issue a new one instead.

Does a device that validates twice take two seats? No - a seat is keyed on the (license, device fingerprint) pair. The second validation reuses the existing seat; no new seat is consumed.

What happens the moment a license passes its grace window? Nothing until it is next validated - there is no background sweeper. The next validation lazily flips it to expired, audits the change, re-signs the certificate, and returns LICENSE_EXPIRED.

How does a service tell a grace-period license from a suspended one? By the result code - a license within grace validates as valid with GRACE_PERIOD, while a suspended one validates as invalid with a status-specific code.

Does each service call licensing on every request? No - a service validates the key once and verifies the returned signed certificate offline. Per-request gating happens against the certificate.

References

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