Skip to content

JUCE activation kit

The Waveform Activation Kit is a C++/JUCE client for audio plugins (VST3, AU, AAX) and desktop apps. It adds key entry, time-limited demos, named-device management with seat swapping, offline Ed25519 verification, and silent background renewal, in a themeable UI that matches your product. It only ever verifies: no signing material ships in your binary.

  • Zero work on the audio thread. The kit never touches processBlock. You read one std::atomic<bool> there. HTTP, file I/O, and cryptography all happen on background or message threads: no locks, allocations, or I/O on the real-time path.
  • Never blocks the UI. Network calls are asynchronous; callbacks arrive on the message thread.
  • Survives teardown. Closing the plugin window mid-request cannot crash or hang the host: in-flight requests are abandoned promptly (bounded by a 10 second timeout) and late callbacks do nothing. The manager is owned by your processor, not the editor.
  • Works offline. After one activation the signed license verifies locally on every launch. When a network is present the kit checks in quietly (on the first license check after load, then on the product’s cadence), renewing the lease. With no network, a full license keeps working for 30 days after its last check-in, with a reminder in the last 7; a demo gets one day.
  • Fails open on ambiguity, closed on proof. If it cannot read its own files, or the network is down on a still-valid license, the user keeps working. Only a cryptographically verified expiry or revocation locks anything.
  • No dependencies you do not already have. JUCE modules you already use (juce_core, juce_events, juce_graphics, juce_gui_basics), plus a vendored public-domain Ed25519 verifier. No OpenSSL, no package manager.

The kit is delivered as files. Waveform sends you one zip per platform and JUCE major, each with a .sha256 checksum file:

waveform-licensing-<version>-<platform>-juce<N>.zip platform: macos | windows N: 7 | 8 | 9

Each zip holds a static library (Release and Debug), headers, a CMake package, a manifest, and a README. JUCE is not bundled; your project supplies it. There is no Linux archive.

Check it before use, from the folder holding both files:

Terminal window
shasum -a 256 -c waveform-licensing-<version>-macos-juce8.zip.sha256 # macOS
sha256sum -c waveform-licensing-<version>-windows-juce8.zip.sha256 # Linux / Git Bash
JUCE The -juce<N> archive must match your JUCE major. The -juce8 archive additionally needs JUCE 8.0.13 or later (8.0.13 changed JUCE’s Component layout, so an older 8.x fails to link or crashes). The exact JUCE an archive was built against is recorded in its manifest.json (juceTag) and is the recommended one. find_package refuses a wrong major at configure time and names the fix.
Windows x64. Visual Studio 2022 17.14 or later, or Visual Studio 2026. Dynamic CRT: /MD for Release, /MDd for Debug (CMake’s default). A static-CRT (/MT) plugin needs a bespoke build; ask.
macOS Universal (arm64 and x86_64), deployment target 11.0 or later.
Language C++17.
# 1) JUCE first: the kit's package needs JUCE's targets to exist already.
add_subdirectory(JUCE)
# 2) The unpacked kit.
find_package(WaveformLicensing CONFIG REQUIRED
PATHS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/waveform-licensing" NO_DEFAULT_PATH)
# 3) Link the imported target.
target_link_libraries(MyPlugin PRIVATE WaveformLicensing::WaveformLicensing)

The imported target carries C++17, the JUCE module links, and the compile definitions the library was built with (JUCE_WEB_BROWSER=0, JUCE_STANDALONE_APPLICATION=0, JUCE_USE_CURL=0). Release and Debug are both in every zip; the right one is chosen per configuration. Unpacked kits contain no secrets, so committing them is fine. For Projucer projects, add the zip’s include/ to the header paths, link lib/Release/ or lib/Debug/, set those three definitions yourself, and check your JUCE version by hand.

Three come from Waveform (the Integration page of your dashboard). None is secret; they ship inside your binary.

  • apiBaseUrl: https://api.waveformanager.com
  • publicKeyB64: your merchant Ed25519 public key
  • productId: the product’s internal ID

You choose one yourself: appFpSalt, any constant string unique to this product. It salts the device fingerprint. Never change it after a release: doing so invalidates every existing activation.

  1. The processor owns the manager and the gate flag. Keep the manager in the processor, not the editor, so a request in flight when the user closes the window is never destroyed mid-call.

    MyAudioProcessor::MyAudioProcessor() /* : ...buses... */ {
    waveform::Config cfg;
    cfg.apiBaseUrl = "https://api.waveformanager.com";
    cfg.productId = "<your productId>"; // required: an entitlement boundary, see below
    cfg.publicKeyB64 = "<your merchant public key>";
    cfg.appFpSalt = "myplugin-fp-salt-v1"; // unique per product; never change
    cfg.vendorName = "Your Company";
    cfg.productName = "Your Plugin";
    cfg.dataSubfolder = "YourCompany/YourPlugin";
    cfg.demoDays = 14; // match the product's admin setting (0 = no demo)
    cfg.purchaseUrl = "https://yourstore.com/yourplugin";
    cfg.supportUrl = "https://yourstore.com/support";
    // cfg.useDeactivateSeatPolicy(); // only if the product's seat policy is "deactivate"
    cfg.onLicenseChanged = [this] { publish(); }; // follow BACKGROUND renewals and revocations
    licenseManager = std::make_unique<waveform::LicenseManager>(cfg);
    recheck(); // verify the stored license offline at startup
    startTimer(60'000); // re-verify every 60 s so expiry and revocation take effect while open
    }
  2. The audio thread reads one atomic, nothing else.

    void MyAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) {
    // ... your DSP ...
    if (licensed.load(std::memory_order_relaxed)) { unlicensedSamples = 0; return; }
    applyUnlicensedDegradation(buffer); // a grace window, THEN intermittent degradation
    }

    Never call refresh(), current(), or any other manager method from processBlock. They allocate and do I/O. Publish with licenseManager->isUsable() from the message thread, and have the audio thread only read the atomic.

  3. The editor shows the gate, or your UI. waveform::ActivationComponent handles the whole flow: auto-formatted key entry, the demo path, “device limit reached, pick one to deactivate, retry”, success, and the re-verify screen if an offline lease lapsed. Add it full-bleed when ! isUsable(), and republish the atomic when it finishes.

Wire onLicenseChanged, or the gate lags the license

Section titled “Wire onLicenseChanged, or the gate lags the license”

refresh() is not the only thing that changes the verdict. The manager also changes it in the background: a forced recheck on first load renews a demo or lapsed lease, a later recheck drops a revoked license, and activate, demo, and self-deactivate complete asynchronously. cfg.onLicenseChanged fires on the message thread after each of those. Republish your atomic from it. It must never call refresh().

productId is an entitlement boundary, not a label

Section titled “productId is an entitlement boundary, not a label”

A signing key is not per product: one key signs tokens for many products. With cfg.productId set, a token carrying a different product ID is rejected offline as product-mismatch. Leave it empty and any token signed with the same key verifies, including a free demo token for another product. It is also sent on activate and heartbeat, which lets the merchant switch on the server’s strict product binding.

A demo is usable, so the startup gate never appears for it. Give users an on-demand way to open the same ActivationComponent: set LicenseStatusBar::onClick so the “Demo, N days left” pill becomes a button, and create the gate from a method you can call at startup and on demand.

Customers can release devices from inside the plugin, on any machine, without a portal. ActivationComponent::showDeviceManager() jumps straight to the list; set LicenseStatusBar::showWhenActive = true to show an “Activated” pill that the same click target can open. The screen marks This computer, asks for a confirming second press, releases this machine for free, and releases another machine through the budgeted path on deactivate-policy products.

cfg.publicKeyB64 = "<current merchant public key>";
cfg.publicKeysB64.add("<next merchant public key>"); // accepted as well

A build that accepts the new key must ship and reach the field before the server signs with it. See Signing keys and rotation.

Only colors, fonts, and the logo change; the flow and layout are fixed. Set cfg.theme before constructing the manager, with tokens background, surface, accent, text, textSubdued, danger, success, cornerRadius, fontName, logoText. A “Powered by Waveform” badge shows unless your plan grants white-label.

The CMake package includes the build-time key check: call waveform_verify_license_key(...) and a drifted key fails your configure step instead of shipping.

The standard archive ships with the kit’s code-signature self-check off (manifest.json has "integrityCheck": "off"); setting macTeamId or winSignerSubject has no effect with it. An integrity-enabled build is a separate, per-vendor artifact. See Hardening for what to add around the kit.