License your app
Gate paid features with signed licence files checked offline, test every state with the testing kit, and check a customer's file from the command line.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Your app will check a signed licence file offline, switch the features you sell on and off, and keep serving when the licence expires, when the network is down and when a revocation hasn't arrived. You will pin a root key, name the licensed features, gate them in code, test each state, and check a file with the CLI. It takes about 20 minutes.
You need
- A MainSDK app with a
main.yamland aprojectid (see Thirty-minute path). - A product name, such as
acme-desktop, that your licences name. - A root public key from your issuer. While you build, use the well-known test root
ed25519:CvBKNOIhJSGvXg9chN9qV8-z6byK3-is6-12mXaidWQ.mainsdk checkrefuses it in production.
1. Declare the licence
licence:
product: acme-desktop
roots: ["ed25519:CvBKNOIhJSGvXg9chN9qV8-z6byK3-is6-12mXaidWQ"] # the test root; swap for yours before you ship
file: ./main.licence
features: [sso, audit-export]features lists every name that needs a licence. A name you pass to the code and don't list is refused. The default driver is offline: it reads the file and makes no network call.
2. Gate the feature
import { licence } from "mainsdk";
export async function GET() {
licence.require("sso"); // throws MAIN-LICENCE-0001 (403) outside valid and grace
return Response.json({ ok: true });
}Use licence.has("audit-export") to hide a button, and licence.state() to show how many days are left. Both read memory only. Gate only what you sell: the app always starts and serves, and nothing else depends on the licence.
3. Test every state
import { it } from "node:test";
import assert from "node:assert/strict";
import { createClock, createTestApp, createTestLicenceCA } from "@mainsdk/testing";
const yaml = `schema: 1
project: prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY
licence:
product: acme-desktop
roots: ["ed25519:CvBKNOIhJSGvXg9chN9qV8-z6byK3-is6-12mXaidWQ"]
features: [sso, audit-export]
`;
it("serves sso in grace and turns it off after", async () => {
const clock = createClock(Date.UTC(2026, 10, 1, 12));
const app = createTestApp({ yaml, clock });
const ca = await createTestLicenceCA({ product: "acme-desktop", project: "prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY" });
await app.licence.install(await ca.issue({ features: ["sso"] }), { scope: "app" }); // valid until 2027-11-01, 14d grace
assert.equal(app.licence.has("sso"), true);
clock.advance("370d");
assert.equal(app.licence.state().status, "grace");
assert.equal(app.licence.has("sso"), true);
clock.advance("20d");
assert.equal(app.licence.state().status, "expired");
assert.throws(() => app.licence.require("sso"), { code: "MAIN-LICENCE-0001" });
});ca.revoke(...), ca.crl() and ca.tamper(token, how) cover revocation and tampering. See the licence interface.
4. Check a customer's file
npx mainsdk licence show ./main.licence # claims, chain, newest revocation list and state
npx mainsdk licence verify ./main.licence --strict # exit 0 valid, 1 not valid or in grace, 2 usageRun verify in CI against a sample licence, or on a customer's file before you send a support reply. A licence bound to a machine needs the machine's id: the customer runs npx mainsdk licence show --machine and sends you the m_… value.
5. Keep installs current
For online installs, use the http driver so renewals and revocations arrive without a release:
licence:
driver: http
url: https://licences.acme.dev # your issuer's refresh endpoint; added to network.allow
refresh: 6hThe app refreshes in the background after it starts listening, never while a request waits. For air-gapped installs, keep offline and copy a new licence file and a licence.trust bundle in; licence.update(token) installs a token at run time and needs the licence.manage permission. Give online installs short licences that refresh, because an app that can't reach the endpoint never learns of a revocation.
6. Before you ship
Replace the test root with your own and check:
npx mainsdk check --env prodA pass exits 0 with no MAIN-LICENCE finding. Keep the ownership clear: the CA's private key stays with your issuer, never in the app.
Check it worked
npx mainsdk licence verify ./main.licenceexits 0.- The test above passes:
has("sso")is true in grace and false after. npx mainsdk check --env prodreports noMAIN-LICENCE-0011and noMAIN-LICENCE-0014.
If it fails
| Code | Cause | Fix |
|---|---|---|
MAIN-LICENCE-0001 | The licence doesn't list the feature, or isn't valid or in grace | Run mainsdk licence show; install a licence that lists it |
MAIN-LICENCE-0004 | The file doesn't verify: wrong root, product or project, an older licence, or an edited byte | Run mainsdk licence verify --json and read reason; ask the issuer for a new file |
MAIN-LICENCE-0007 | No file at licence.file, or not valid yet | Put the file there, or set licence.token |
MAIN-LICENCE-0011 | The licence section is invalid | Fix the key the finding names |
MAIN-LICENCE-0012 | The code names a feature that isn't under licence.features | Add it, or fix the typo |
MAIN-LICENCE-0014 | The test root is pinned in production | Pin your own root |
What it can't do
It keeps honest customers honest and makes your terms auditable. It can't stop someone who controls the machine from patching the SDK, replacing the pinned root, wiping the runtime store or staying offline so a revocation never arrives. Serve features worth stealing from a server you run.
For agents
Goal: gate the features we sell with signed licences.
Read: docs/specs/licence.md, this page, main.yaml.
Touch only: main.yaml (licence section), the routes that gate a feature, tests.
Do: add the licence section with product, roots and features; call licence.require or licence.has for each
licensed feature; test valid, grace, expired and tampered with createTestLicenceCA.
Verify: npx mainsdk check --json --agent; npx mainsdk licence verify <file>
Never pin the test root for production, never put a CA private key or a licence token in code, and never gate
anything that isn't listed under licence.features.Next
- App licensing, the interface reference
- Licensing: app licences compared with TAPS licences, API keys and entitlements
- Limits, metering and entitlements
Was this page helpful?