Skip to main content

License States

Every license in KeyBox progresses through a defined lifecycle with four possible states:
~/workspace/source/apps/server/src/models/License.ts
export enum Status {
  PENDING = "PENDING",
  ACTIVE = "ACTIVE",
  EXPIRED = "EXPIRED",
  REVOKED = "REVOKED",
}

State Descriptions

PENDING

License created but not yet activated by the end user

ACTIVE

License activated and currently valid

EXPIRED

License has passed its expiration date

REVOKED

License manually revoked by the developer

State Transition Diagram

PENDING State

When a license is first created, it starts in the PENDING state.

Characteristics

  • License key is generated and stored in database
  • Expiration date is calculated but not yet enforced
  • No machine binding exists
  • License cannot be validated until activated

Creation Code

~/workspace/source/apps/server/src/controllers/license.controller.ts
const license = await License.create({
  key,
  duration,
  issuedAt,
  expiresAt,
  status: Status.PENDING, // Default state
  services: services || ["Hosting"],
  user: req.userId,
  client: clientId,
  project: projectId,
});

Validation Response

{
  "valid": false,
  "status": "pending",
  "message": "License has not been activated yet"
}
Users receive the license key immediately but must activate it through your application or the SDK.

ACTIVE State

A license transitions to ACTIVE when the end user activates it for the first time.

Activation Process

1

User Initiates Activation

Application calls the activation endpoint with the license key
2

Machine ID Captured

System generates a unique, hashed machine identifier
3

License Bound

License is bound to the machine and status changes to ACTIVE
4

Expiration Timer Starts

The expiration countdown begins from activation time

Activation Code

~/workspace/source/apps/server/src/controllers/redisLicense.controller.ts
export const activateLicense = async (req: Request, res: Response) => {
  const { key } = req.body;
  const machineId = machineIdSync(true);
  
  const license = await License.findOne({ key });
  
  if (license.status === Status.PENDING) {
    // First-time activation
    const issuedAt = new Date();
    const expiresAt = new Date();
    expiresAt.setMonth(expiresAt.getMonth() + license.duration);
    
    license.status = Status.ACTIVE;
    license.issuedAt = issuedAt;
    license.expiresAt = expiresAt;
    license.machineId = machineId; // Bind to this machine
    
    await license.save();
    await invalidateCachedLicense(key);
  }
}

Validation Response

{
  "valid": true,
  "status": "active",
  "duration": "6 months",
  "expiresAt": 1735689600000
}
Once activated, a license is permanently bound to the machine ID. It cannot be transferred to another machine without developer intervention.

EXPIRED State

Licenses automatically transition to EXPIRED when the current date exceeds expiresAt.

Expiration Methods

A scheduled cron job runs periodically to mark expired licenses:
~/workspace/source/apps/server/src/api/cron/expired-licenses.ts
const expiredLicenses = await License.find({
  expiresAt: { $lt: now },
  status: Status.ACTIVE,
});

await License.updateMany(
  { key: { $in: expiredLicenses.map((l) => l.key) } },
  { $set: { status: Status.EXPIRED } }
);

// Invalidate cache for each expired license
for (const license of expiredLicenses) {
  await invalidateCachedLicense(license.key);
}
The cron job ensures licenses are marked as expired even if they’re not being actively validated.
Licenses are also checked during validation:
if (license.status === Status.ACTIVE) {
  const now = new Date();
  if (now > license.expiresAt) {
    license.status = Status.EXPIRED;
    await license.save();
    return res.json({
      valid: false,
      status: "expired",
      message: "License has expired",
      expiresAt: license.expiresAt,
    });
  }
}
This provides real-time expiration enforcement without waiting for the cron job.

Validation Response

{
  "valid": false,
  "status": "expired",
  "message": "License has expired",
  "expiresAt": 1735689600000
}

Expired License Behavior

  • License key remains in database for record-keeping
  • Cannot be reactivated (new license required)
  • Machine binding is preserved
  • All validation requests return valid: false

REVOKED State

Developers can manually revoke licenses at any time, regardless of current state.

Revocation Use Cases

  • Suspected license abuse or sharing
  • Refund issued to customer
  • Policy violation by licensee
  • Emergency license termination

Toggle Implementation

~/workspace/source/apps/server/src/controllers/license.controller.ts
export const toggleLicense = async (req: Request, res: Response) => {
  const key = req.params.key;
  const license = await License.findOne({ key });
  
  license.status = 
    license.status === Status.ACTIVE 
      ? Status.REVOKED 
      : Status.ACTIVE;
  
  await license.save();
  
  // Invalidate cache because state CHANGED
  await invalidateCachedLicense(key);
  
  return res.json({
    message: `License status changed to ${license.status}`,
    key: license.key,
    status: license.status,
  });
}
Revoking a license immediately invalidates all active sessions. Applications using the SDK will shut down on the next validation check.

Validation Response

{
  "valid": false,
  "status": "revoked",
  "message": "License revoked by developer"
}

SDK Behavior on Revocation

Applications using the Node.js SDK will automatically shut down:
~/workspace/source/apps/SDK/Node-SDK/index.js
const terminalStatuses = ["revoked", "expired", "invalid"];

if (isTerminal && lastState !== "invalid") {
  lastState = "invalid";
  log("ERROR", `License ${statusLower.toUpperCase()} — shutting down", data);
  onRevoke?.(data);
  return;
}

State Comparison Table

StateValidCan ActivateMachine BoundReversible
PENDINGNoYesNoYes (to REVOKED)
ACTIVEYesN/AYesYes (to REVOKED/EXPIRED)
EXPIREDNoNoYesNo
REVOKEDNoNoPreservedYes (toggle back)

Cache Invalidation Strategy

State changes trigger cache invalidation to ensure consistency:
// Always invalidate cache on state transitions
await invalidateCachedLicense(key);

When Cache is Invalidated

  • License activation (PENDING → ACTIVE)
  • License revocation (ACTIVE/PENDING → REVOKED)
  • License expiration (ACTIVE → EXPIRED)
  • Toggle operations (ACTIVE ↔ REVOKED)
Cache invalidation ensures validation requests always receive the latest license state within milliseconds.

Best Practices

Monitor Pending Licenses

Track licenses that remain PENDING for extended periods—they may indicate delivery issues

Automate Expiration

Rely on the cron job for consistent expiration enforcement

Use Revocation Sparingly

Revocation immediately impacts users—consider communication before action

Preserve Expired Records

Don’t delete expired licenses—they provide valuable audit history

Next Steps

Machine Binding

Learn how licenses are bound to specific devices

API Reference

Explore the validation and activation endpoints

Build docs developers (and LLMs) love