Offline Licensing for Your VST/AU Plugin Without a Backend
Audio plugin piracy is not an abstraction. Walk through any plugin forum and you will find working cracks of commercial releases from studios with full licensing implementations. The crackers are ahead, and no licensing system promises to close that gap permanently. What licensing does is raise the effort required and make direct purchase the obviously easier path — and that is enough to matter commercially.
What makes audio plugins genuinely tricky from a licensing standpoint is not the piracy threat specifically. It is the constraints: your plugin loads inside someone else’s process, sessions are frequently offline, and the audio thread is real-time-critical, which means a blocking call to verify a license in processBlock is not just wrong — it is a hard bug that causes dropouts.
The Keylight JUCE adapter solves the audio-thread constraint cleanly. You hold a keylight::juce_integration::Licensing member in your processor, activate from the plugin editor on the message thread, and gate features in processBlock with hasFeature("pro") — a single lock-free atomic read, no allocation, safe under real-time constraints. Here is how to set it up.
What you are actually solving
Two things need to be true for plugin licensing to work:
Offline sessions must work. Recording sessions happen on isolated machines, on the road, in studios with deliberately air-gapped production networks. If your plugin fails to initialize because it cannot reach a server, you ship a plugin that fails in professional environments. Signed offline leases solve this: the plugin verifies a tamper-proof Ed25519 signature against a bundled public key at every launch — no network dependency on the critical path.
The audio thread cannot block. processBlock runs under real-time constraints. Any I/O on that thread — a network call, a disk read, a mutex — is a potential dropout. The check you put in processBlock must be a lock-free read. Move everything else off the audio thread.
The single-header adapter handles both: offline lease verification happens at initialization on a background thread, and processBlock reads an atomic bool.
Adding the adapter
Drop keylight_single.hpp into your JUCE project’s Source/ directory. No CMake changes, no new dependencies — the adapter uses juce::URL for HTTP, which your JUCE build already provides.
Initialize in your PluginProcessor
#include "KeylightJuce.h" // the single-header JUCE adapter
class MyPluginAudioProcessor : public juce::AudioProcessor
{
public:
MyPluginAudioProcessor()
: licensing(makeConfig())
{
// Verify any cached lease offline, then refresh in the background.
// Returns immediately — never blocks the constructor.
licensing.checkOnLaunch();
}
// Owns the keylight::Client, the JUCE transport + on-disk store, and the
// lock-free atomic the audio thread reads via hasFeature().
keylight::juce_integration::Licensing licensing;
private:
static keylight::Config makeConfig()
{
keylight::Config cfg;
cfg.tenantId = "your-tenant";
cfg.productId = "your-plugin";
// Trusted Ed25519 public keys (kid -> base64) from your dashboard.
cfg.trustedKeys["k1"] = "<base64-public-key>";
return cfg;
}
};
Licensing wraps keylight::Client with JUCE’s own networking and an on-disk lease store. checkOnLaunch() verifies any cached lease against the trusted keys and resolves state without a full activation round-trip — all off the message thread.
Activate from the plugin editor
Activation belongs in your UI — a text field, an activate button, in your editor class:
void MyPluginEditor::activateButtonClicked()
{
auto key = licenseKeyField.getText().trim();
// The callback is delivered on the message thread — touch UI directly.
processor.licensing.activate(key,
[this](keylight::Result<keylight::State> r)
{
if (r.is_ok() && r.value() == keylight::State::Licensed) {
statusLabel.setText("Licensed", juce::dontSendNotification);
updateProUI(processor.licensing.hasFeature("pro"));
} else {
statusLabel.setText("Activation failed — check your key.",
juce::dontSendNotification);
}
});
}
activate() runs the round-trip off the message thread and invokes your callback back on the message thread — no manual thread marshaling. Licensing updates the audio-thread flag internally once the state is confirmed.
Gate features in processBlock
void MyPluginAudioProcessor::processBlock(
juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& midiMessages)
{
// One lock-free atomic read — safe on the audio thread.
if (!licensing.hasFeature("pro"))
{
// Free path: pass through dry signal, silence pro bus, etc.
applyFreeFeatures(buffer);
return;
}
// Pro path: full algorithm
applyProFeatures(buffer);
}
This is the complete audio-thread pattern. There is no other code path you add to processBlock for licensing. Licensing writes the flag from its background thread; the audio thread only reads it via hasFeature(), with no locking.
States and what to show in the UI
Licensing::state() exposes four states, readable from the message thread:
| State | What to show |
|---|---|
Licensed | ”Licensed” — hide activation UI |
Trial | ”Trial — X days remaining” |
Expired | ”License expired — renew” |
Invalid | Activation input + button |
Read state() in your editor constructor to show the correct initial UI. To update in real time, set the adapter’s onStateChanged callback — Licensing invokes it on the message thread whenever the state changes.
How the offline path works
After a successful activate() call, the adapter persists the Ed25519-signed lease to disk using the JUCE application data directory. On every subsequent load — across any DAW host, any session, any reboot:
checkOnLaunch()loads the cached lease from disk.- It verifies the Ed25519 signature against your configured trusted public key.
- If valid and not expired, it resolves
Licensedand updates the atomic.
No network call on that path — the entire startup sequence is local. Background revalidation runs periodically when connectivity is available: it refreshes the lease TTL and propagates revocations. A refunded key transitions to Invalid on the next successful revalidation contact, not during an active session.
What Keylight handles
Keylight sits between Stripe and your plugin. When a customer completes checkout:
- Stripe fires a webhook to Keylight.
- Keylight mints a signed license key and delivers it by email.
- The customer enters the key in your plugin’s activation UI.
activate()callsapi.keylight.dev, the server returns a signed lease, the adapter stores it.- Every subsequent launch verifies locally.
You configure your product in the Keylight dashboard, connect Stripe, and drop in the SDK key. There is no webhook server for you to run.
The takeaway
Plugin licensing has two hard constraints — offline sessions and the real-time audio thread — and the single-header adapter addresses both directly. One header file in your source tree, one checkOnLaunch() call in your constructor, one activate() call from your editor, and one hasFeature() read in processBlock.
For the complete integration reference — multi-entitlement setup, revalidation tuning, the offline lease format — see Licensing for audio plugins.
If you hit a JUCE version quirk or DAW-specific behavior this post does not cover, send your feedback and I’ll extend it.
— Nicolas Demanez — Founder
Frequently asked
Is the license check in processBlock safe on the audio thread?+
Yes. The check reads a std::atomic<bool> — a single lock-free load, no allocation, no mutex. All network and disk I/O happens on a background thread managed by the adapter.
Does one license key work across Ableton, Logic, and FL Studio?+
Yes. The license is tied to the customer and their device activations, not to a specific host. The plugin reads the same cached lease regardless of which DAW loads it.
Do I need a server?+
No. The adapter calls api.keylight.dev at activation and for background revalidation. Keylight mints, signs, tracks, and revokes licenses. Your plugin only holds the public key used to verify leases locally.
Ready to ship?
Create your account and start licensing your apps in under a minute. Free forever tier included.
Start Free