Licensing · Guides

Setup

#Getting a token

A license token comes from a Threadplane plan — grab one from threadplane.ai/pricing. Once you have it, paste it into environment.threadplaneLicense or set THREADPLANE_LICENSE in your environment, and the rest of this page wires it up. The nag warnings the library emits point at the same URL.

Most applications don't import @threadplane/licensing directly. @threadplane/chat calls runLicenseCheck() from provideChat() when you pass a license token.

Chat exposes a license option:

provideChat({ license: environment.threadplaneLicense });

When no token is supplied, the licensing helper can evaluate the environment as noncommercial when the caller passes isNoncommercial: true.

#Direct use

Use the direct API when you're building a package inside the framework, or a custom integration that needs the same behavior.

import {
  LICENSE_PUBLIC_KEY,
  inferNoncommercial,
  runLicenseCheck,
} from '@threadplane/licensing';
 
const status = await runLicenseCheck({
  package: '@threadplane/example',
  token: process.env['THREADPLANE_LICENSE'],
  publicKey: LICENSE_PUBLIC_KEY,
  isNoncommercial: inferNoncommercial(),
});

runLicenseCheck() returns the evaluated LicenseStatus.

To see the status in action without a real token, mint one inline with signLicense() and log what runLicenseCheck() resolves to:

import * as ed from '@noble/ed25519';
import { signLicense, runLicenseCheck } from '@threadplane/licensing';
 
// Stand-in for the minting service: a throwaway keypair.
const privateKey = ed.utils.randomPrivateKey();
const publicKey = await ed.getPublicKeyAsync(privateKey);
 
const nowSec = Math.floor(Date.now() / 1000);
const token = await signLicense(
  {
    sub: 'cus_123',
    tier: 'developer_seat',
    iat: nowSec,
    exp: nowSec + 60 * 60 * 24 * 365,
    seats: 1,
  },
  privateKey,
);
 
const status = await runLicenseCheck({
  package: '@threadplane/example',
  token,
  publicKey,
});
console.log(status); // 'licensed'

In production you verify against LICENSE_PUBLIC_KEY instead of a throwaway key, and the token comes from your environment — not from signLicense().

#Warnings

emitNag() is silent for:

  • licensed;
  • noncommercial.

It warns once per package and status for:

  • missing;
  • grace;
  • expired;
  • tampered.

The warning prefix is [threadplane], and the package keeps running.

You can inject a custom warning sink:

await runLicenseCheck({
  package: '@threadplane/example',
  token,
  publicKey,
  warn: (message) => logger.warn(message),
});

#Noncommercial hint

inferNoncommercial() checks globalThis.process?.env.NODE_ENV.

It returns:

  • true when NODE_ENV exists and is anything other than "production";
  • false when NODE_ENV is "production";
  • false when there is no process global.

It's only a default hint. Callers can pass isNoncommercial explicitly.

#Gotchas

runLicenseCheck() is idempotent for identical package and token values. A repeated call with the same key returns licensed without re-running the check.

That keeps repeated provider initialization quiet, but it means package authors shouldn't use repeated calls with identical inputs to poll for status.

verifyLicense() doesn't check time. Pair it with evaluateLicense() when you need expiration and grace behavior.