Domain Model
All five tables live in the
licensingPostgreSQL schema and are defined in@nx/coreatpackages/core/src/models/schemas/licensing/.@nx/licensingre-exports the repositories only.
1. Full ERD
2. Entities
Policy
| Property | Value |
|---|---|
| Table | licensing.Policy |
| Source | packages/core/src/models/schemas/licensing/policy/schema.ts |
| Soft-delete | yes (generateCommonColumnDefs) |
| Owner ID column | — (system-level template) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
name | jsonb (i18n) | ✓ | — | Display name { default, en, vi } |
description | jsonb (i18n) | — | Optional | |
product | text | ✓ | — | Product identifier (indexed) |
type | text | ✓ | — | PolicyTypes (see enum) |
status | text | ✓ | ACTIVATED | PolicyStatuses |
sequence | integer | ✓ | 0 | Display order |
duration | jsonb (IDuration) | — | { unit, value }; null = perpetual | |
activation | jsonb (IActivationConfig) | — | { limit }; null = unlimited devices | |
gracePeriod | jsonb (IDuration) | — | Extends validation window after expiry |
type enum (PolicyTypes):
| Value | Description |
|---|---|
000_TRIAL | Trial plan (the seeded free trial uses this) |
100_SUBSCRIPTION | Time-bounded subscription |
200_PERPETUAL | No expiry (set duration: null) |
status enum (PolicyStatuses): ACTIVATED · DEACTIVATED · ARCHIVED.
Indexes: IDX on product, type, status.
PolicyFeature
| Property | Value |
|---|---|
| Table | licensing.PolicyFeature |
| Source | packages/core/src/models/schemas/licensing/policy-feature/schema.ts |
| Soft-delete | no (generateTzColumnDefs({ deleted: { enable: false } })) |
| Owner ID column | policyId |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
policyId | text | ✓ | — | FK → Policy.id (cascade delete) |
code | text | ✓ | — | Feature key (e.g. MAX_ALLOCATION_LAYOUTS) |
dataType | text | ✓ | — | boolean / number / text / json (from generateDataTypeColumnDefs) |
boValue | boolean | — | Value when dataType=boolean | |
nValue | numeric | — | Value when dataType=number | |
tValue | text | — | Value when dataType=text | |
jValue | jsonb | — | Value when dataType=json | |
name | jsonb (i18n) | ✓ | — | Display name |
description | jsonb (i18n) | — | Optional | |
sequence | integer | ✓ | 0 | Display order |
status | text | ✓ | ACTIVATED | PolicyFeatureStatuses (ACTIVATED/DEACTIVATED) |
Polymorphic value:
LicensingBaseService.DATA_TYPE_RESOLVERSreads the column matchingdataType. ADEACTIVATEDfeature resolves to the per-type default (false/0/''/null).
Indexes & constraints: UQ on (policyId, code); IDX on policyId, status; FK cascade on delete.
License
| Property | Value |
|---|---|
| Table | licensing.License |
| Source | packages/core/src/models/schemas/licensing/license/schema.ts |
| Soft-delete | yes |
| Owner ID column | entityType + entityId (polymorphic principal) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
policyId | text | ✓ | — | FK → Policy.id |
key | text | ✓ | — | License key (PREFIX-XXXX-XXXX-XXXX-XXXX) |
name | jsonb (i18n) | ✓ | — | Display name |
status | text | ✓ | ACTIVATED | LicenseStatuses (see enum) |
entityType | text | ✓ | — | LicensePrincipalTypes: Merchant / User |
entityId | text | ✓ | — | Principal id |
certificate | text | — | Latest signed cert envelope (base64) | |
override | jsonb | — | { activation, features } — overrides Policy defaults | |
issuedAt | timestamptz | ✓ | now() | — |
startsAt | timestamptz | ✓ | now() | Validity start |
expiresAt | timestamptz | — | null = perpetual | |
graceExpiresAt | timestamptz | — | End of post-expiry grace window | |
lastValidatedAt | timestamptz | — | Updated (fire-and-forget) on each successful validate |
status enum (LicenseStatuses):
| Value | Description |
|---|---|
ACTIVATED | Active and usable |
SUSPENDED | Temporarily disabled (reinstatable) |
EXPIRED | Past expiresAt + grace (set lazily on validate) |
REVOKED | Permanently terminated |
isActive=ACTIVATEDonly;isInactive={SUSPENDED, REVOKED, EXPIRED}.
Indexes & constraints: partial-unique UQ on key (where deletedAt IS NULL); IDX on policyId, status, (entityType, entityId), entityId, expiresAt; FK → Policy.id.
Activation
| Property | Value |
|---|---|
| Table | licensing.Activation |
| Source | packages/core/src/models/schemas/licensing/activation/schema.ts |
| Soft-delete | yes |
| Owner ID column | licenseId |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
licenseId | text | ✓ | — | FK → License.id (cascade delete) |
fingerprint | text | ✓ | — | Device fingerprint |
label | text | — | Friendly device name | |
platform | text | — | OS / platform | |
hostname | text | — | Device hostname | |
ip | text | — | Last-seen IP |
Indexes & constraints: partial-unique UQ on (licenseId, fingerprint) (where deletedAt IS NULL) — one activation per device per license; IDX on licenseId; FK cascade.
LicenseEvent
| Property | Value |
|---|---|
| Table | licensing.LicenseEvent |
| Source | packages/core/src/models/schemas/licensing/license-event/schema.ts |
| Soft-delete | no (append-only audit) |
| Owner ID column | licenseId (nullable) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
licenseId | text | — | FK → License.id (ON DELETE SET NULL) | |
event | text | ✓ | — | LicenseEventTypes (created/activated/deactivated/suspended/reinstated/renewed/expired/revoked) |
ip | text | — | — | |
userAgent | text | — | — | |
data | jsonb | ✓ | {} | Event-specific payload |
metadata | jsonb | — | — |
Indexes: IDX on licenseId, event; FK ON DELETE SET NULL (events survive license deletion).
3. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
Activation count ≤ activation.limit | ActivationService.activate / ValidationService.tryCreateActivation: SELECT License FOR UPDATE → re-COUNT inside tx → rollback if >= limit |
One activation per (licenseId, fingerprint) | Partial-unique index + find-before-create fast path |
| License key unique among live rows | Partial-unique index on key WHERE deletedAt IS NULL |
Feature value matches dataType | DATA_TYPE_RESOLVERS reads the typed column; DEACTIVATED → per-type default |
renew requires a Policy duration | RENEW_PERPETUAL error if duration is null |
| Certificate reflects current license state | Re-published after every lifecycle mutation (issue/suspend/reinstate/renew/revoke) and on lazy expiry |
4. Soft-delete Behavior
| Entity | Soft-delete | Hard-delete | Notes |
|---|---|---|---|
Policy | yes (deletedAt) | via repo | PolicyController deletes by status (no hard delete in app) |
License | yes | via repo | Read default deletedAt IS NULL |
Activation | yes | cascade from License (FK) | Deactivate uses deleteById |
PolicyFeature | no | cascade from Policy (FK) | Deleted column disabled |
LicenseEvent | no | FK SET NULL on license delete | Append-only audit |