Licensing
Sell your app with signed licence files checked offline, and tell app licences apart from TAPS licences, API keys and entitlements.
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.
An app licence is your signed permission for one install or one tenant to use your app's features. You issue it from your MainSDK Cloud account; your app checks it offline against a public key you pin in main.yaml. Nothing calls home while a request runs, and the app keeps serving when nothing is reachable (ADR-0070).
import { licence } from "mainsdk";
licence.require("sso"); // 403 unless the licence in scope includes sso
const { status, graceEndsAt } = licence.state();licence:
driver: http
product: acme-desktop
roots: ["ed25519:CvBKNOIhJSGvXg9chN9qV8-z6byK3-is6-12mXaidWQ"] # the well-known test root: pin your own in production
file: ./main.licence
features: [sso, audit-export]Four things that sound alike
| App licence | TAPS licence | API key | Entitlement | |
|---|---|---|---|---|
| What it answers | May this install or tenant use this feature of the app? | May this person open this protected file, now? | Which customer is calling the app's API? | What does this tenant's plan include? |
| Who grants it | You, the vendor, from MainSDK Cloud | A TAPS Node, under the file's policy | Your app, to its own customers | Your app, from its plans and the tenant's subscription |
| Who holds it | The customer's install or tenant, as a file or token | The person's open session | The customer's code | Nobody: it is a check in your code |
| How long it lasts | Months, renewed by refresh or a new file | Seconds, renewed every protect.renew | Until rotated or revoked | As long as the plan |
| Checked where | In the app, offline, against a pinned root | By the TAPS Node, online | In the app, against a SHA-256 digest | In the app, from memory |
| Interface | licence | protect | apikeys | entitlements |
| Call | licence.require("sso") | protect.open(asset, { purpose }) | apikeys.verify(req) | entitlements.require("export") |
| When it can't be checked | Valid stays valid; only expiry plus grace stops it | Nothing opens | The request is refused | The last known plan is served |
They work together. A feature you sell under licence goes under licence.features; a tenant then has it only when its plan includes it and the licence does too, and a licence's seats or limits can lower a plan's limit but never raise it. A tenant licence can also name the tenant's plan. An app licence may carry a reference to a TAPS licence or asset for your own use, but licence never evaluates it and protect never reads app licences.
How a licence is checked
Each licence file holds two signed parts: the licence, signed by your issuing CA, and the CA's certificate, signed by your root. Your app pins the root's public key, so it can check the whole chain with no network. Everything is Ed25519 on the platform's WebCrypto, in a DSSE envelope over canonical JSON, so one changed byte fails.
| State | Licensed features | When |
|---|---|---|
valid | On | Signed by a pinned chain, inside its dates, not revoked |
grace | On, with a daily warning | Expired, or revoked or suspended by a signed list, within its grace |
expired, revoked, suspended | Off | After grace |
tampered | Off | Doesn't verify: signature, chain, format, wrong project or product, wrong environment or machine, older than one already seen, or the test root in production |
missing | Off | No licence, or not valid yet |
Only licensed features turn off. The app always starts and serves, and no other protection depends on a licence.
Revocation and refresh
You revoke or suspend a licence in the Cloud, which publishes a signed revocation list. With the http driver your app fetches new lists and renewed licences in the background, never while a request waits. With the offline driver you copy a trust file in. An unreachable list never blocks a valid licence, so offline revocation is bounded by the licence's own expiry: give online installs short licences that refresh.
The app remembers the newest licence, the newest list and the latest time it has seen, so an older licence, an older list or a clock set backwards doesn't bring an expired licence back.
What it promises, honestly
A licence can't be forged or edited without your CA's private key, which never leaves the Cloud's key service, and an app that has seen something newer can't be rolled back through anything MainSDK reads. That keeps honest customers honest and makes your terms auditable.
MainSDK is open source and runs on your customer's machine. Someone who controls that machine can patch the check out or keep the machine offline. The seal reports changed files, but it can be rewritten too. Serve features worth stealing from a server you run.
Try it locally
mainsdk dev uses the local driver: with no licence file, it serves a dev licence with every feature under licence.features, signed by a test root that mainsdk check refuses in production. In tests, createTestLicenceCA() from @mainsdk/testing issues, revokes and tampers with licences. To inspect a file, run mainsdk licence show main.licence, and mainsdk licence verify in CI.
Next
- App licensing: the calls,
main.yamlkeys and testing kit - License your app: the steps, start to finish
- Free in the app, paid in the Cloud
- Security model
- Check codes
Was this page helpful?