Skip to main content

What is Machine Binding?

Machine binding is KeyBox’s mechanism for enforcing single-device license activation. When a user activates a license, it becomes permanently associated with that specific machine’s unique identifier.
This prevents license sharing and ensures each license is used on only one device at a time.

How Machine IDs Work

KeyBox uses the node-machine-id library to generate stable, unique identifiers for each machine.

Machine ID Generation

~/workspace/source/apps/server/src/controllers/redisLicense.controller.ts
import { machineIdSync } from "node-machine-id";

// Generate stable, hashed machine ID
const machineId = machineIdSync(true);

Machine ID Characteristics

Stable

Same ID across application restarts and system reboots

Unique

Different for each physical/virtual machine

Hashed

SHA-256 hashed for privacy and security

Hardware-Based

Derived from hardware identifiers (CPU, MAC, etc.)

What Machine IDs Include

The library generates IDs based on:
  • CPU serial number
  • System UUID
  • MAC address
  • Motherboard serial number
The exact components vary by operating system. The machineIdSync(true) parameter ensures the ID is hashed for privacy.

License Binding Process

Machine binding occurs during first activation:
1

User Receives License Key

Developer generates a license in PENDING state with no machine binding
2

User Activates License

Application calls /validate/activate endpoint with the license key
3

Machine ID Captured

Server generates the machine ID for the requesting device
4

License Bound

Machine ID is stored in the license record permanently
license.status = Status.ACTIVE;
license.machineId = machineId; // Permanent binding
await license.save();

Activation Code with Binding

~/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); // Get current machine ID
  
  const license = await License.findOne({ key });
  
  // Check if already activated on different machine
  if (license.status === Status.ACTIVE) {
    if (license.machineId !== machineId) {
      return res.status(403).json({
        success: false,
        message: "License already activated on another machine",
      });
    }
    
    return res.json({
      success: true,
      message: "License already activated on this machine",
    });
  }
  
  // First-time activation - bind to this machine
  license.status = Status.ACTIVE;
  license.machineId = machineId;
  await license.save();
}
Once a license is activated, the machine binding is permanent and cannot be changed without developer intervention.

Machine ID Validation

Every license validation request checks the machine ID:
~/workspace/source/apps/server/src/controllers/redisLicense.controller.ts
export const validateLicense = async (req: Request, res: Response) => {
  const { key } = req.body;
  const currentMachineId = machineIdSync(true);
  
  const license = await License.findOne({ key });
  
  // Machine mismatch check
  if (
    license.status === Status.ACTIVE &&
    license.machineId &&
    license.machineId !== currentMachineId
  ) {
    return res.json({
      valid: false,
      status: "machine_mismatch",
      message: "License is not valid for this machine",
    });
  }
}

Validation Flow

Machine Mismatch Response

{
  "valid": false,
  "status": "machine_mismatch",
  "message": "License is not valid for this machine"
}

Data Model

Machine ID storage in the license schema:
~/workspace/source/apps/server/src/models/License.ts
const licenseSchema = new Schema<LicenseType>({
  key: {
    type: String,
    unique: true,
    required: true,
  },
  // ... other fields ...
  machineId: {
    type: String,
    default: null,    // Null until activation
    index: true,      // Indexed for fast lookups
  },
});

export interface LicenseType {
  key: string;
  duration: number;
  issuedAt: Date;
  expiresAt: Date;
  status: Status;
  services: Services[];
  machineId: string;  // Bound machine identifier
  user: mongoose.Types.ObjectId;
  client: mongoose.Types.ObjectId;
  project: mongoose.Types.ObjectId;
}
The machineId field is indexed to enable fast machine-based license lookups and queries.

Caching Machine IDs

Machine IDs are included in cached license data:
~/workspace/source/apps/server/src/cache/license.cache.ts
export interface CachedLicense {
  status: Status;
  message?: string;
  expiresAt?: number;
  duration?: string;
  machineId?: string; // Cached for fast validation
}

// When caching an active license
await setCachedLicense(key, {
  status: Status.ACTIVE,
  expiresAt: license.expiresAt.getTime(),
  duration: `${license.duration} months`,
  machineId: license.machineId, // Include in cache
});

Cache Validation

Redis cache hits also enforce machine binding:
const cached = await getCachedLicense(key);
if (cached) {
  // Check machine mismatch from cache
  if (
    cached.status === Status.ACTIVE &&
    cached.machineId &&
    cached.machineId !== currentMachineId
  ) {
    return res.json({
      valid: false,
      status: "machine_mismatch",
      message: "License is not valid for this machine",
    });
  }
}
Caching machine IDs allows KeyBox to enforce device restrictions without database queries, maintaining sub-millisecond validation times.

Security Implications

Benefits

Users cannot share license keys with others, as each key only works on one machine.
Ensures compliance with single-device licensing agreements automatically.
Prevents unauthorized distribution and use of licenses across multiple devices.
Machine IDs provide a record of which device activated each license for support and security purposes.

Privacy Considerations

Machine IDs are hashed using SHA-256 before storage. Raw hardware identifiers are never stored or transmitted.
  • Hashed IDs cannot be reverse-engineered to reveal hardware details
  • No personally identifiable information (PII) is included
  • Machine IDs are only used for validation—not tracking or analytics

SDK Machine Binding

The Node.js SDK automatically handles machine binding:
~/workspace/source/apps/SDK/Node-SDK/index.js
export async function activateLicense({
  productName,
  key,
  apiUrl = "https://api-keybox.vercel.app",
  endpoint = "/validate/activate",
}) {
  // SDK sends license key to activation endpoint
  // Server automatically captures and binds machine ID
  const res = await fetch(`${apiUrl}${endpoint}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ key, productName }),
  });
  
  const data = await res.json();
  
  if (!res.ok || data?.success === false) {
    throw new Error(data?.message || "License activation failed");
  }
  
  return data; // Includes machineId in response
}
SDK users don’t need to manually handle machine IDs—the activation process is automatic and transparent.

Handling Edge Cases

Hardware Changes

Machine IDs can change when:
  • Motherboard is replaced
  • CPU is upgraded
  • Virtual machine is migrated
  • MAC address changes
In these scenarios, users will receive a machine_mismatch error. Developers must manually reset the license’s machine binding.

Virtual Machines

Virtual machines have stable machine IDs as long as:
  • VM configuration remains unchanged
  • VM is not cloned or duplicated
  • Hypervisor provides consistent hardware identifiers
Cloning VMs will generate different machine IDs. Each clone requires a separate license.

Development vs Production

For development/testing scenarios:
// Option: Create licenses without machine binding
const devLicense = await License.create({
  // ... standard fields ...
  machineId: null, // No binding for dev licenses
});
Consider creating separate “development” license types that don’t enforce machine binding for internal testing.

Machine Binding Lifecycle

StateMachine IDBehavior
PENDINGnullNo machine binding—can be activated on any device
ACTIVE (first use)Set on activationBound to the activating machine
ACTIVE (subsequent)UnchangedValidates against bound machine
EXPIREDPreservedMachine binding retained for records
REVOKEDPreservedMachine binding retained for records

Best Practices

Communicate Binding Policy

Inform users during purchase that licenses are single-device only

Provide Transfer Mechanism

Build a support process for legitimate machine changes

Monitor Mismatch Errors

Track machine_mismatch responses to identify abuse patterns

Test Across Platforms

Verify machine ID stability on Windows, macOS, and Linux

Resetting Machine Bindings

To allow a license to be re-activated on a new machine:
// Reset machine binding through database or API
await License.findOneAndUpdate(
  { key: licenseKey },
  { 
    $set: { 
      machineId: null,
      status: Status.PENDING 
    } 
  }
);

// Invalidate cache
await invalidateCachedLicense(licenseKey);
Only reset machine bindings for legitimate support cases. This process should require authentication and authorization.

Next Steps

License Lifecycle

Understand how licenses transition between states

SDK Integration

Integrate machine-bound licenses into your application

API Reference

Explore validation and activation endpoints

Dashboard

Monitor license activations and machine bindings

Build docs developers (and LLMs) love