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.
Design promise: it stays out of your way
Section titled “Design promise: it stays out of your way”- Zero work on the audio thread. The kit never touches
processBlock. You read onestd::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.
Getting the kit
Section titled “Getting the kit”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 | 9Each 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:
shasum -a 256 -c waveform-licensing-<version>-macos-juce8.zip.sha256 # macOSsha256sum -c waveform-licensing-<version>-windows-juce8.zip.sha256 # Linux / Git BashRequirements
Section titled “Requirements”| 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. |
Add it to your CMake build
Section titled “Add it to your CMake build”# 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.
Your constants
Section titled “Your constants”Three come from Waveform (the Integration page of your dashboard). None is secret; they ship inside your binary.
apiBaseUrl:https://api.waveformanager.compublicKeyB64: your merchant Ed25519 public keyproductId: 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.
Wire it up
Section titled “Wire it up”-
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 belowcfg.publicKeyB64 = "<your merchant public key>";cfg.appFpSalt = "myplugin-fp-salt-v1"; // unique per product; never changecfg.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 revocationslicenseManager = std::make_unique<waveform::LicenseManager>(cfg);recheck(); // verify the stored license offline at startupstartTimer(60'000); // re-verify every 60 s so expiry and revocation take effect while open} -
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 fromprocessBlock. They allocate and do I/O. Publish withlicenseManager->isUsable()from the message thread, and have the audio thread only read the atomic. -
The editor shows the gate, or your UI.
waveform::ActivationComponenthandles 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.
Let users enter a key during a demo
Section titled “Let users enter a key during a demo”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.
Manage devices
Section titled “Manage devices”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.
Accepting more than one signing key
Section titled “Accepting more than one signing key”cfg.publicKeyB64 = "<current merchant public key>";cfg.publicKeysB64.add("<next merchant public key>"); // accepted as wellA build that accepts the new key must ship and reach the field before the server signs with it. See Signing keys and rotation.
Theming
Section titled “Theming”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.
Build-time key check
Section titled “Build-time key check”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.
Integrity self-check
Section titled “Integrity self-check”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.