TL;DR
We built a licensing system that cryptographically binds software licenses to specific domains. This post walks through the architecture decisions, security model, and real-world patterns we use in production.
The Problem
Most software licensing systems rely on API keys or serial numbers. The issue? These are trivially shareable. One customer buys a license, shares the key on a forum, and suddenly your entire customer base is using a single license.
We needed something better: a system where licenses are cryptographically bound to specific domains, making unauthorized sharing technically impractical.
Architecture Overview
Our licensing system has three core layers:
- License Generation — Creates signed, domain-bound tokens
- Validation Engine — Verifies licenses both online and offline
- Enforcement Layer — Handles grace periods, trial expiry, and usage metering
License Structure
Each license is a signed JWT containing:
{
"sub": "lic_abc123",
"domain": "example.com",
"plan": "professional",
"features": ["analytics", "webhooks", "custom-domains"],
"limits": { "apiCalls": 50000, "seats": 10 },
"iat": 1711929600,
"exp": 1743465600
}
The domain binding is the critical piece. When a license is validated, we compare the requesting domain against the domain claim. Mismatches are rejected.
Domain Binding: How It Works
The binding happens at two levels:
1. Server-Side Validation
When your application calls our API to validate a license:
const response = await fetch('https://api.example.com/v1/licenses/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
licenseKey: 'lic_abc123',
domain: window.location.hostname
})
})
const { valid, features, limits } = await response.json()
The API checks:
- Is the license key valid and not expired?
- Does the requesting domain match the bound domain?
- Is the license within its usage limits?
- Has the customer's subscription been canceled or suspended?
2. Offline Validation
For applications that need to work without internet access, we provide offline validation using public key cryptography:
import { verify } from './license-verifier'
const result = verify({
licenseKey: 'lic_abc123',
publicKey: EMBEDDED_PUBLIC_KEY,
currentDomain: window.location.hostname
})
if (result.valid) {
enableFeatures(result.features)
}
The license token is signed with our private key. The SDK ships with the corresponding public key, so it can verify the signature without calling home.
SDK Architecture
We ship SDKs for 9 languages:
| Language | Package Manager | Validation |
|---|---|---|
| Node.js | npm | Online + Offline |
| Python | PyPI | Online + Offline |
| Go | Go modules | Online + Offline |
| Ruby | RubyGems | Online |
| Java | Maven Central | Online + Offline |
| .NET | NuGet | Online + Offline |
| Rust | crates.io | Online + Offline |
| PHP | Packagist | Online |
| Django | PyPI | Online (middleware) |
Each SDK follows the same pattern:
# Python example
from traffic_orchestrator import LicenseClient
client = LicenseClient(api_key='your_api_key')
# Validate on every request (middleware pattern)
result = client.validate(
license_key='lic_abc123',
domain='customer-app.com'
)
if result.valid:
print(f'Plan: {result.plan}, Features: {result.features}')
else:
print(f'Invalid: {result.error}')
Security Model
Several layers prevent abuse:
Rate Limiting
Validation endpoints are rate-limited per API key. Excessive requests trigger progressive delays, not hard blocks — we don't want to break legitimate applications.
Replay Protection
Each validation response includes a nonce. Replaying a captured response fails because the nonce won't match.
Certificate Pinning
Our SDKs pin the API's TLS certificate. This prevents MITM attacks where someone might try to intercept validation requests and return spoofed "valid" responses.
Webhook Notifications
When suspicious activity is detected (validation from unexpected domains, burst requests), we fire webhooks to the customer's configured endpoint:
{
"event": "license.suspicious_activity",
"data": {
"license_id": "lic_abc123",
"reason": "domain_mismatch",
"requested_domain": "pirated-site.com",
"bound_domain": "legitimate-app.com"
}
}
Lessons Learned
1. Grace Periods Matter
Don't immediately kill access when a license expires. We give a 7-day grace period where the license still works but returns "grace": true in the response. This gives developers time to renew without breaking their users' experience.
2. Offline-First Design
We assumed most customers would always have internet. Wrong. Many enterprise customers deploy behind firewalls. Offline validation went from "nice to have" to "critical feature" within our first month.
3. Feature Flags > Plan Names
Instead of checking plan === 'pro', we expose individual feature flags. This decouples your code from our pricing model. When we restructure plans, your code doesn't break.
// Bad: Coupled to plan names
if (license.plan === 'professional' || license.plan === 'enterprise') {
enableAdvancedFeatures()
}
// Good: Decoupled via feature flags
if (license.features.includes('advanced-analytics')) {
enableAdvancedFeatures()
}
4. Usage Metering Must Be Async
Don't block the validation response while recording usage. We fire-and-forget usage events to a queue, which are processed asynchronously. This keeps validation latency under 100ms.
What's Next
We're working on:
- Terraform provider (already live!) for infrastructure-as-code license management
- Usage-based billing integration with Stripe
- Multi-domain licenses for enterprise customers with multiple properties
If you're building a SaaS product and struggling with license management, we'd love to hear about your use case. Drop a comment or check out our documentation.
What licensing challenges have you faced in your projects?
Top comments (0)