Plug-in licensing
Sell a native plug-in with Maru activation in one command: seats, trials, offline grace, refunds, tester copies and the dry-through gate, with no licensing code in your module.
Maru can license the VST3, AU, CLAP and standalone builds of your module. Buyers sign in once from the plug-in, approve the computer in their browser, and keep working offline. You choose how many computers a license covers and how long the trial runs. Maru issues, checks and withdraws the licenses. When a purchase is refunded, its activations end at the plug-in's next check.
In the Help Center: Licenses and product access for what buyers experience when they activate.
You write no licensing code. The plug-in shell handles activation, background checks, the activation panel and the audio gate.
Quick start
In your module's folder:
maru publish # once, if you have not published yet: links the module to your Studio project maru module license on # turns licensing on and binds the build maru plugin build --release # every release build now asks buyers to activate
maru module license on finds your Studio project by the module's id (the
id in defineEffect or defineInstrument). It enables licensing and keeps
your project's current seat and trial settings; a new project starts with
3 computers per license and a 14-day trial. An explicit --project must match
that module's linked Studio project. Then it writes one line into your project:
// package.json
"maru": {
"entry": "src/index.ts",
"license": { "provider": "maru", "projectId": "<your project id>" }
}
For a project with a plugin.config.json, the line goes there instead, as
"commercialLicense": { "provider": "maru", "projectId": "…" }. Commit it with
your code.
Choose other limits with flags:
maru module license on --seats 2 --trial 0
The flags set computers per license (1–10) and trial days (0, the default 14, or up to 30). You can change these at any time without rebuilding. A plug-in that is already activated keeps its seat, and trials already started keep their length.
You can also do all of this in Studio: open your module, then Settings › Plug-in licensing. It shows the same settings and the command to copy.
Development builds stay unlicensed
Only release builds (maru plugin build --release) include licensing.
Development builds stay unlicensed so they process audio without an
activation:
maru plugin buildwithout--release;maru dev;- format checks.
The build log says which kind it made.
| You want to | Do this |
|---|---|
| Try activation in a development build | MARU_LICENSE=1 maru plugin build |
| Build a release without licensing (an internal or free build) | MARU_LICENSE=0 maru plugin build --release |
A release build refuses to compile without Maru's public signing keys, so a licensed build can never accept a forged license.
Sell it and test it
-
Price it. Set the price on your module's listing as for any product. A purchase grants the buyer the module, and that grant is their license. A refund removes it.
-
Test it yourself. As the module's creator, you own its license without buying it. Activate a release build in your DAW as a buyer would. The same seat limit applies to you.
-
Give testers a copy:
maru module license grant tester@example.com maru module license revoke tester@example.com
The tester needs a Maru account. A module can have 25 testers, and Studio lists them under Plug-in licensing. Revoking a tester's copy stops their computers at the next check, unless they have since bought it.
Check the numbers:
maru module license status
It shows your settings, live seats (owned and trial), trials started, activations and refusals over 30 days by format, and your testers. It also checks that the binding in your project matches the project on Maru.
You see counts only. Buyers' computers, countries and networks stay private.
To stop licensing, run maru module license off. It turns licensing off and
removes the binding. A binding left behind would build a plug-in nobody can
activate.
Recover a failed setup
If the online settings were saved but writing your local file failed, the CLI reports both outcomes and prints a repair command. Fix the local file or its write permissions, then check the current state from your module's folder:
maru module license status --json
Status identifies a missing binding, a binding left behind after licensing was turned off, or a binding for a different product. Use the matching local repair:
maru module license on --project YOUR_PROJECT_ID --local-only # online licensing is on maru module license off --project YOUR_PROJECT_ID --local-only # online licensing is off
--local-only reads the current online settings and repairs only the local
binding. It does not change seat limits, trials or tester grants, and repeating
it does not create another license. It refuses --seats or --trial, and stops
if licensing has changed to the opposite on/off state. Use the same account,
profile and API origin as the failed command. Review a binding for a different
product before changing it; the CLI preserves conflicting SKU bindings.
If the configure request lost its response, its online outcome can be unknown: check status before deciding to configure again. A damaged or unreadable local JSON file must be repaired before status can load your module. Another local writer or an online policy change can create new drift, so check status again before building the licensed release.
Show the license in your face (optional)
The shell draws its own activation badge and panel, so a face needs nothing.
If you want the state in your own design, read license from the face props.
It is present only in a licensed native build:
import type { AudioModuleFaceProps } from "maruaudio/contracts"
export function Face({ license, ...props }: AudioModuleFaceProps) {
return (
<header>
{license?.state.status === "trial" ? (
<button onClick={license.activate}>Trial · {license.state.daysLeft} days left — Buy</button>
) : null}
{license && ["unlicensed", "expired", "invalid"].includes(license.state.status)
? <span>Activation required</span> : null}
</header>
)
}
license.state.status is one of:
unchecked,unlicensed,licensed,trial;grace(offline 16–30 days);expired,invalid,unavailable.
license.active is true for licensed, trial and grace. It describes the
reported license status, rather than measuring whether audio is processing.
For example, initialization and service-unavailable states currently keep the
effect processing while reporting active: false. The actions are:
activate();cancel();deactivate();refresh()("Check now");openAccount().
Each action is answered by a new license. Display fields cover:
- the trial's days left;
- seats used and total;
- which computers hold the seats when they are all used.
No token, key or account id ever reaches the face.
The current source behavior for licensed effect builds is:
| Status | license.active | Effect processing | Recovery action |
|---|---|---|---|
unchecked | false | Continues while the local token is read | Wait for the initial check; investigate a state that does not settle |
licensed | true | Full processing | Check now, manage computers or deactivate |
trial | true | Full processing until the recorded trial end | Buy and check again, or manage the activation |
grace | true | Full processing within the cached token window | Go online and check again |
unlicensed | false | Dry-through | Activate or manage exhausted seats |
expired | false | Dry-through | Check current ownership or activate again |
invalid | false | Dry-through | Resolve the reported token/device/clock error and activate again |
unavailable | false | Continues; no definitive refusal is established | Resolve the service/configuration failure and retry |
This describes the current host contract, not release qualification. Startup bounds and unavailable-state behavior still require the protected-release acceptance. Instruments currently keep processing, and browser/unlicensed hosts omit the license capability. A custom face cannot change the host's entitlement decision or audio gate; the standard shell activation panel remains available.
Check a license from your own server
Use this for support diagnostics and license-status display. A buyer's plug-in
holds a maru-license/1 token. Verification does not authenticate the requester
or prove possession of that computer's private key: a copied token receives the
same status. A protected download or update service needs a separate scoped
authentication or device-proof operation; this public lookup does not provide it.
Live check: asks Maru whether the token is genuine and still held now. It needs no account and names no person:
curl -s https://maruaudio.com/api/v1/licenses/verify \
-H 'content-type: application/json' \
-d '{ "token": <token>, "sku": "creator:module:<project id>", "bundle": "<bundle id>" }'
The answer is { "ok": true, "result": { "valid": true, "license": { … } } }.
When valid is false, reason is one of:
invalid_tokenorexpired;ended(the computer was removed or deactivated);not_entitled(refunded);wrong_skuorwrong_bundle.
Offline check: verifies the signature and dates without a network call, in Node 20+, Workers, Deno or a browser:
import { verifyLicenseToken } from "@maruaudio/platform-auth-contract"
const keys = [{ kid: "prod-2026-10", x963: "04add2c939…" }] // license-keys.json, set "production"
const result = await verifyLicenseToken(token, keys, {
sku: "creator:module:<project id>",
bundle: "<bundle id>",
})
const status = result.ok ? result.status : "invalid"
console.log(status)
result.ok establishes signature and token-shape checks. A correctly signed
token may still have status expired; inspect the status and bind both expected
product identities. This result grants no request authority. An offline check
cannot see a refund or a removed computer until the token expires (at most
30 days). The live lookup checks current ownership and activation records when
that distinction matters.
What your buyers see
| State | What the plug-in does |
|---|---|
| Not activated | An Activate badge. An effect passes audio through unchanged. |
| Trial | A Trial · N days left badge, full processing. One trial per account. |
| Licensed | Full processing and a small License button (deactivate, manage computers). |
| Offline 16–30 days | Still working, with a reminder to go online. |
| Offline over 30 days, refunded, or removed | Passes audio through unchanged until it checks again. |
The activation panel lives in the plug-in window. It belongs to the shell, so it looks the same in every Maru plug-in and does not touch your face's code. Approval happens in the system browser. No password or token ever enters the plug-in window.
Desktop sign-in is currently separate from this activation flow. It does not activate every owned plugin; start activation in the plugin that needs it.
Buyers see and remove their computers at Plug-in activations.
Unactivated instruments keep full processing for now. Only effects have the dry-through gate. Ask before shipping a licensed instrument.
How a license works
-
Activation. The plug-in creates its own random key and signs in as the
maru-pluginclient, with scopelicenses:activateonly. It then asks/api/v1/licenses/activatefor a seat. The server checks two things:- that the account owns the product, or still has its trial;
- that a seat is free.
-
The license token. The server returns a
maru-license/1token, signed with Maru's key and bound to that computer's key. The plug-in checks it offline against public keys compiled into the build. A copied token does nothing on another computer. -
Renewal. The plug-in renews about once a day in the background. Each request is signed by the plug-in's own key, so the account sign-in is not needed again. Ownership is checked on every renewal, so a refund ends the license:
- within a day, for a computer that is online;
- within 30 days, for one that never connects.
-
Privacy. Maru stores:
- a hash of the device key;
- a one-way digest of the computer's hardware id (the id itself never leaves the computer);
- the computer name;
- the country and network of each check.
It never stores IP addresses. See the Privacy Policy.
Building outside the Maru shell
The licensing wire contract is public in @maruaudio/platform-auth-contract
(plugin-license.ts). A host other than the Maru plug-in shell can implement
it:
| Call | Auth | Body | Answer |
|---|---|---|---|
POST /api/v1/licenses/activate | Bearer syn_… with licenses:activate | sku, bundle, devicePublicKey (P-256 point, base64url), componentDigest, platform, label, os, format, clientVersion | { token, seats } |
POST /api/v1/licenses/refresh | device proof | token, proof, devicePublicKey | { token, seats } |
POST /api/v1/licenses/deactivate | device proof | token, proof, devicePublicKey | { ended: true } |
POST /api/v1/licenses/verify | none | token, optional sku, bundle | { valid, reason?, license? } |
Every answer is { ok: true, result } or
{ ok: false, error: { code, message } }. Codes include:
seat_limit(with the computers holding the seats);trial_usedandnot_entitled;revokedandseat_removed;rate_limited.
The signature rules:
- Tokens and proofs are signed with ECDSA P-256 / SHA-256 as raw 64-byte
r‖s, over the ASCII string
maru-license/1\n<base64url payload>. Proofs usemaru-license-req/1\n…instead. - A proof's payload is
{ op, lid, ts, nonce }.tsmust be within 5 minutes of server time, and a nonce works once. - Verify the token against the production public key in
packages/platform-auth-contract/license-keys.json, or withverifyLicenseToken.
The golden vectors in packages/platform-auth-contract/vectors/ show the
exact bytes.
Support and limits
- Sales and refunds: your Earnings page lists them. A refund ends that buyer's activations.
- A buyer locked out of a computer: they remove it at Plug-in activations and activate again. Maru support can also free a seat for them.
- Platforms: macOS (Apple silicon and Intel) for now. Windows and offline activation files are planned.