PRD: Plans, Licenses & Runtime Entitlements
| Module | Licensing (CORE-15) | PRD ID | PRD-PLN-001 |
| Status | Shipped | Owner | Licensing squad |
| Date | 2026-06-05 | Version | v1.0 |
| Packages | @nx/licensing · @nx/core | URD | PLN · LIC · VAL |
TL;DR
Lets platform operators sell KICKO as plans that bundle typed feature flags and seat limits, issue a license from a plan to a merchant, and drive that license through a tracked lifecycle. At runtime, any service validates a license key and receives a signed certificate it trusts offline - so feature gating is consistent and auditable without a live call to licensing on every request. The outcome: one shared, trustworthy definition of "what this merchant paid for," with merchants able to self-serve a single free trial.
1. Context & Problem
KICKO is sold as plans that bundle different feature sets and seat limits, and the product needs a single, trustworthy way to decide what each merchant is entitled to run. Without it, every module re-invents ad-hoc gating and there is no shared definition of "what this merchant paid for" - making entitlement inconsistent, unauditable, and impossible to reason about across services.
This increment establishes the core licensing spine: reusable plans carrying typed feature flags, licenses issued from those plans with a tracked lifecycle, and a runtime validation call that returns a signed certificate other services verify offline - so a consumer never needs a live call to licensing on every request. Billing and payment are deliberately out of scope; this increment is about entitlement, not money.
2. Goals & Non-Goals
Goals
- Operators define reusable plans (trial / subscription / perpetual) with duration, grace period, seat limit, and typed feature flags.
- Issue a license from a plan, bound to a merchant or user, and move it through renew / suspend / reinstate / revoke with a full audit trail.
- Validate a license key at runtime and return its status, resolved features, and a signed certificate consumers verify offline.
- Expose a public plan catalog for sign-up and upgrade surfaces.
- Let a merchant self-serve exactly one free trial without operator intervention.
Non-Goals
- Billing, invoicing, dunning, or payment-charge integration.
- Usage metering, heartbeats, or eager/scheduled expiry sweeping.
- Per-feature runtime enforcement inside consuming modules - each module gates itself against the certificate.
- A license-management UI (frontend concern).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Offline gating | Every consuming service gates features against the certificate - zero live licensing calls per request |
| Free-trial integrity | Free-trial issuance succeeds idempotently; one merchant never ends up with two trials |
| Audit completeness | 100% of lifecycle actions produce an append-only event and a re-signed certificate |
| Validation correctness | Validation outcomes (valid / grace / expired / suspended / revoked) match the license state |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Platform Operator | Define plans & feature flags; issue, renew, suspend, reinstate, revoke licenses |
| Merchant Owner | Browse the plan catalog, self-serve one free trial, run on a valid license |
| Consuming service | Validate a license key and gate features & seats offline against the certificate |
Core scenarios: operator defines a plan with feature flags → a merchant is issued (or self-serves) a license → a service validates the key and receives status + resolved features + a signed certificate → it gates features and seats offline → operator renews / suspends / reinstates / revokes, re-signing the certificate each time.
5. User Stories
- As a platform operator, I want to define reusable plans with typed feature flags and a seat limit, so I can package what each tier of merchant may run.
- As a platform operator, I want to issue a license from a plan bound to a merchant or user, so the entitlement is tied to a known principal with a unique key.
- As a platform operator, I want to renew, suspend, reinstate, and revoke a license, so I can manage its lifecycle with a full audit trail.
- As a merchant owner, I want to self-serve a single free trial, so I can evaluate the product without operator involvement and without ending up with duplicate trials.
- As a merchant owner, I want a public catalog of active plans, so I can choose or upgrade my plan.
- As a consuming service, I want to validate a license key and receive a signed certificate, so I can gate features and seats offline without calling licensing on every request.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Create a plan of type trial / subscription / perpetual, with a duration (or null perpetual) and an optional grace period | URD-PLN-001..002 |
| FR-2 | A plan optionally caps devices (seat limit); null = unlimited | URD-PLN-003 |
| FR-3 | Attach typed feature flags (boolean / number / text / json); a deactivated flag resolves to its type's empty/off default | URD-PLN-004..005 |
| FR-4 | Expose a public catalog of active plans with their active features, in display order | URD-PLN-008 |
| FR-5 | Issue a license from a plan, bound to a merchant or user, with a unique generated key; compute expiry from duration + grace | URD-LIC-001..002 |
| FR-6 | Suspend then reinstate; renew (extends validity, restores an expired license); revoke (terminal) | URD-LIC-004..005 · URD-LIC-007 |
| FR-7 | A merchant self-serves a single free-trial license; an existing trial is returned, never duplicated | URD-LIC-008 |
| FR-8 | Every lifecycle action is recorded in an append-only event log | URD-LIC-009 |
| FR-9 | Each lifecycle change re-signs and re-publishes the license certificate | URD-LIC-010 |
| FR-10 | Validate a key and return whether it is valid plus a result code | URD-VAL-001 |
| FR-11 | Validation returns the resolved feature set with per-license overrides applied | URD-VAL-006 |
| FR-12 | A successful validation returns a signed certificate other services verify offline | URD-VAL-008 |
Full requirement text and acceptance criteria live in the Licensing URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Each lifecycle change writes an audit event and re-signs the certificate together - no state change without a matching audit entry |
| Immutability | The license event log is append-only and survives license deletion |
| Tenancy & authz | A license is bound to exactly one principal (merchant or user); plan/license management is gated by licensing-scoped permissions |
| Performance / scale | Consumers verify the certificate offline; no live licensing call per request. Expiry is detected lazily on validation - no background sweeper |
| Trust | The certificate is signed (Ed25519) and verifiable offline by any service |
| i18n | Plan display name and description are bilingual ({ en, vi }) |
8. UX & Flows
Backend-only surface in @nx/licensing (five controllers over plans, feature flags, licenses, activations, validation). There is no license-management UI in this increment; sign-up and upgrade surfaces read the public plan catalog.
9. Data & Domain
| Entity | Role |
|---|---|
Policy | The plan template - type, duration, grace period, seat limit, display metadata |
PolicyFeature | A typed feature flag on a plan (boolean / number / text / json), unique code per plan |
License | An issued entitlement bound to a merchant or user, with a unique key, status, expiry, and optional overrides |
Activation | A binding of a license to one device by fingerprint, counted against the seat limit |
LicenseEvent | An append-only audit record of every lifecycle action |
Conceptual only - full schema and invariants in the licensing domain model. All five tables live in
@nx/coreand are re-exported by@nx/licensing.
10. Dependencies & Assumptions
Depends on
- Commerce (Commerce) - a license is bound to a merchant (or user) principal whose identity comes from Commerce.
- Permissions (Permissions) - plan and license management actions are gated by
licensing-scoped permissions. - A signing key pair (Ed25519) for certificate signing, with the public key distributed to consumers for offline verification.
Assumptions
- Consuming modules do their own per-feature gating against the certificate; licensing does not enforce features inside them.
- A free-trial plan is seeded so merchants can self-serve from day one.
- Expiry checks happen lazily on validation; reporting tolerates eventual (not eager) expiry detection.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Lazy expiry may lag reporting accuracy | Accepted; expiry flips on next validation. Open: introduce a scheduled sweeper if reporting needs eager expiry |
| No read API over the license-event log | Append-only log exists; open question whether operators need a REST surface or direct DB access suffices |
| Fixed 30/365-day duration arithmetic | Open: move to calendar-aware months/years if needed |
| Certificate trust depends on key distribution | Public key shipped to consumers; rotation path must be defined before key change |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (plans + licenses + validation), P2 (activation + free trial + certificate distribution) - see URD feature catalog |
| Rollout | Foundation of @nx/licensing; a free-trial plan is seeded so merchants can self-serve from day one; no feature flag |
| Migration | None (new entities; tables live in @nx/core, free-trial plan seeded at startup) |
| Launch criteria | Issue → validate → certificate verified offline end-to-end; lifecycle actions each produce an audit event + re-signed certificate; free-trial issuance idempotent |
| Monitoring | Validation outcome distribution, free-trial issuance rate, audit-event vs. lifecycle-action consistency, certificate signing failures |
13. FAQ
Does this handle billing or payment? No - this increment is entitlement only. Billing, invoicing, dunning, and payment-charge integration are explicitly out of scope.
Does a service call licensing on every request? No - a service validates the key once and receives a signed certificate it verifies offline. Per-request gating happens against the certificate, not a live licensing call.
What happens when a license expires? Expiry is detected lazily on validation: a license past its grace window is flipped to expired on the next validation and returns invalid. There is no background sweeper.
Can a merchant get two free trials? No - free-trial issuance is idempotent. A merchant who already has a trial gets the existing one back rather than a duplicate.
Who enforces individual features? Each consuming module gates itself against the resolved feature set in the certificate. Licensing defines and signs entitlements; it does not enforce them inside other modules.
References
- URD: Licensing - Plans & Feature Flags · License Lifecycle · Runtime Validation & Entitlements
- Related: Device Activation
- Module: Licensing - overview + traceability
- Developer: @nx/licensing · domain model