DEV Community

Traffic Orchestrator
Traffic Orchestrator

Posted on • Originally published at trafficorchestrator.com

Building a Domain-Bound Software Licensing System: Architecture Deep Dive

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:

  1. License Generation — Creates signed, domain-bound tokens
  2. Validation Engine — Verifies licenses both online and offline
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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}')
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)