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:

sh
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:

json
// 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:

sh
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 build without --release;
  • maru dev;
  • format checks.

The build log says which kind it made.

You want toDo this
Try activation in a development buildMARU_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

  1. 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.

  2. 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.

  3. Give testers a copy:

sh
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:

  • sh
    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:

    sh
    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:

    sh
    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:

    tsx
    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:

    Statuslicense.activeEffect processingRecovery action
    uncheckedfalseContinues while the local token is readWait for the initial check; investigate a state that does not settle
    licensedtrueFull processingCheck now, manage computers or deactivate
    trialtrueFull processing until the recorded trial endBuy and check again, or manage the activation
    gracetrueFull processing within the cached token windowGo online and check again
    unlicensedfalseDry-throughActivate or manage exhausted seats
    expiredfalseDry-throughCheck current ownership or activate again
    invalidfalseDry-throughResolve the reported token/device/clock error and activate again
    unavailablefalseContinues; no definitive refusal is establishedResolve 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:

    sh
    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_token or expired;
    • ended (the computer was removed or deactivated);
    • not_entitled (refunded);
    • wrong_sku or wrong_bundle.

    Offline check: verifies the signature and dates without a network call, in Node 20+, Workers, Deno or a browser:

    ts
    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

    StateWhat the plug-in does
    Not activatedAn Activate badge. An effect passes audio through unchanged.
    TrialA Trial · N days left badge, full processing. One trial per account.
    LicensedFull processing and a small License button (deactivate, manage computers).
    Offline 16–30 daysStill working, with a reminder to go online.
    Offline over 30 days, refunded, or removedPasses 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-plugin client, with scope licenses:activate only. It then asks /api/v1/licenses/activate for 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/1 token, 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:

    CallAuthBodyAnswer
    POST /api/v1/licenses/activateBearer syn_… with licenses:activatesku, bundle, devicePublicKey (P-256 point, base64url), componentDigest, platform, label, os, format, clientVersion{ token, seats }
    POST /api/v1/licenses/refreshdevice prooftoken, proof, devicePublicKey{ token, seats }
    POST /api/v1/licenses/deactivatedevice prooftoken, proof, devicePublicKey{ ended: true }
    POST /api/v1/licenses/verifynonetoken, 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_used and not_entitled;
    • revoked and seat_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 use maru-license-req/1\n… instead.
    • A proof's payload is { op, lid, ts, nonce }. ts must 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 with verifyLicenseToken.

    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.