Skip to content

Architecture

1. System Context (C4 L1)

2. Container View (C4 L2)

No Kafka consumer, BullMQ worker, or WebSocket emitter container exists — the service is a single REST process plus a migration entry.

3. Component View (C4 L3) — Internal Layering

LayerResponsibility
RoutesHTTP surface, declared in RestPaths + per-controller definitions.ts
ControllersControllerFactory.defineCrudController (Policy/PolicyFeature/License/Activation) + custom @post lifecycle handlers; auth + permission gate
ServicesBusiness logic, transactions, certificate re-publish (LicenseManagementService, ActivationService, ValidationService)
LicensingBaseServiceShared base: key generation, feature resolution, publishCertificate(), logEvent(), createActivation()
RepositoriesRe-exported from @nx/core; soft-delete via generateCommonColumnDefs
ComponentsRedis cache only (cert distribution + authz cache)

4. State Machines Index

EntityStatesDiagram
LicenseACTIVATED, SUSPENDED, EXPIRED, REVOKED→ jump

Policy and PolicyFeature use generic lifecycle statuses (ACTIVATED/DEACTIVATED/ARCHIVED) with no guarded transitions. Activation has no status column — its lifecycle is create/delete.

License

FromEventToGuards
* (new)issueACTIVATEDPolicy must exist; key generated; cert published
ACTIVATEDsuspendSUSPENDEDMust be isActive() else SUSPEND_INVALID_STATUS
SUSPENDEDreinstateACTIVATEDMust be SUSPENDED else REINSTATE_INVALID_STATUS
ACTIVATEDvalidate (lazy)EXPIREDexpiresAt < now AND past graceExpiresAt; conditional UPDATE ... WHERE status=ACTIVATED
ACTIVATED/EXPIREDrenewACTIVATEDRejects REVOKED/SUSPENDED; Policy must have a duration (else RENEW_PERPETUAL)
any non-REVOKEDrevokeREVOKEDIdempotent-guard REVOKE_ALREADY_REVOKED

Expiry is lazy. There is no scheduled reaper. A license flips to EXPIRED only when ValidationService.validate() runs and observes that expiresAt and the grace window have both passed.

5. Runtime Scenarios

5.1 Issue a license

5.2 Validate a key (lazy expiry + implicit activation)

5.3 Activate a device (race-safe seat limit)

5.4 Certificate consumption (other services)

6. Crosscutting Concerns

ConcernHow this service handles it
AuthNJWT (Issuer = identity, JWKS) or HTTP Basic — [AuthenticateStrategy.JWT, AuthenticateStrategy.BASIC] on every route
AuthZCasbin via PolicyDefinition; permissions seeded by migrations 0001/0003/0004. License read routes temporarily authorize.skip (ADR-0003)
i18njsonb i18n columns ({ default, en, vi }) on Policy.name/description, PolicyFeature.name/description, License.name
LoggingStructured, key-value (key: %s); per-method scope via this.logger.for(name)
TracingNo-op (no tracer wired)
IdempotencyActivation fast-path (find by licenseId+fingerprint before lock); issueFreeTrial returns existing trial; expiry flip via conditional UPDATE; partial-unique index on (licenseId, fingerprint)
ConcurrencySELECT … FOR UPDATE row-lock on the License row for all lifecycle mutations and activation-limit checks
Soft-deleteLicense, Policy, Activation carry deletedAt (via generateCommonColumnDefs); PolicyFeature & LicenseEvent disable deleted column
IDsSnowflake via IdGenerator, worker 11

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