Go is one of the most popular languages for shipping commercial software — CLI tools, microservices, desktop utilities, embedded agents, and infrastructure software. Its static binary compilation, zero-dependency distribution, and cross-compilation make it the ideal choice for licensed software that needs to run anywhere. This guide walks through implementing license key validation in Go from first principles, building up to a production-grade licensing package with offline support, concurrency safety, and middleware integration.
Why Go Is Ideal for Licensed Software
Before diving into code, it is worth understanding why Go and software licensing fit together so well:
- Static binaries —
go buildproduces a single executable with no runtime dependencies. You ship one file. No installer, no dependency resolution, no "please install .NET 8 first." This makes license enforcement straightforward because there is exactly one artifact to protect. - Cross-compilation —
GOOS=linux GOARCH=arm64 go buildgives you an ARM64 Linux binary from your macOS laptop. Licensed software often targets multiple platforms, and Go eliminates the per-platform build complexity that plagues C/C++ projects. - Embedded systems and IoT — Go binaries run on Raspberry Pi, industrial gateways, and edge devices. These environments frequently operate offline, making Ed25519-based offline validation essential.
- CLI tools — The Go ecosystem powers tools like Docker, Kubernetes, Terraform, and Vault. If you are building commercial CLI software, Go is the de facto standard.
- Microservices — Go dominates the cloud-native space. License validation as HTTP middleware or gRPC interceptor slots naturally into existing service architectures.
Basic License Validation with HTTP Client
The simplest integration is a direct HTTP call to the Traffic Orchestrator validation API. Go's standard library provides everything you need — no third-party HTTP client required.
The Validation Request
Define the request and response types, then make the API call:
package license
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
const apiBaseURL = "https://api.trafficorchestrator.com/api/v3"
// ValidateRequest is the payload sent to the validation endpoint.
type ValidateRequest struct {
LicenseKey string `json:"licenseKey"`
Domain string `json:"domain,omitempty"`
MachineID string `json:"machineId,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// ValidateResponse is the API response from validation.
type ValidateResponse struct {
Valid bool `json:"valid"`
License *License `json:"license,omitempty"`
Error string `json:"error,omitempty"`
}
// License holds the details of a validated license.
type License struct {
ID string `json:"id"`
Key string `json:"key"`
Status string `json:"status"`
Tier string `json:"tier"`
Features []string `json:"features"`
ExpiresAt time.Time `json:"expiresAt"`
MaxDomains int `json:"maxDomains"`
}
// Validate checks a license key against the Traffic Orchestrator API.
func Validate(ctx context.Context, apiKey string, req ValidateRequest) (*ValidateResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
apiBaseURL+"/licenses/validate",
bytes.NewReader(body),
)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var result ValidateResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &result, nil
}
Error Handling with Go Idioms
Go does not use exceptions. Every function returns an error alongside its result, forcing the caller to handle failures explicitly. This is ideal for license validation where you need to distinguish between "license invalid" (a business logic result) and "network failure" (an infrastructure problem):
result, err := license.Validate(ctx, apiKey, license.ValidateRequest{
LicenseKey: key,
Domain: "app.example.com",
})
if err != nil {
// Infrastructure failure: network timeout, DNS error, API down.
// Decide whether to fail open or fail closed.
log.Printf("validation error: %v", err)
return handleValidationError(err)
}
if !result.Valid {
// Business logic: key is expired, revoked, or does not exist.
return fmt.Errorf("license invalid: %s", result.Error)
}
// License is valid — proceed with the licensed operation.
log.Printf("license %s valid, tier: %s", result.License.ID, result.License.Tier)
This two-level error handling — separating transport errors from validation results — is a pattern you will see throughout this guide. It maps naturally to Go's error philosophy: errors are values, not control flow.
Production-Grade Middleware
Most Go applications are web services. Rather than calling Validate() in every handler, wrap license checking into middleware that runs before your business logic.
Standard Library (net/http)
package middleware
import (
"context"
"net/http"
"strings"
"yourapp/license"
)
type contextKey string
const licenseContextKey contextKey = "license"
// RequireLicense returns middleware that validates the license key
// from the X-License-Key header on every request.
func RequireLicense(apiKey string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-License-Key")
if key == "" {
key = strings.TrimPrefix(
r.Header.Get("Authorization"), "LicenseKey ",
)
}
if key == "" {
http.Error(w, "{"error":"missing license key"}", http.StatusUnauthorized)
return
}
result, err := license.Validate(r.Context(), apiKey, license.ValidateRequest{
LicenseKey: key,
Domain: r.Host,
})
if err != nil {
http.Error(w, "{"error":"validation service unavailable"}", http.StatusServiceUnavailable)
return
}
if !result.Valid {
http.Error(w, "{"error":"invalid license"}", http.StatusForbidden)
return
}
// Attach the license to the request context for downstream handlers.
ctx := context.WithValue(r.Context(), licenseContextKey, result.License)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// GetLicense extracts the validated license from the request context.
func GetLicense(ctx context.Context) *license.License {
l, _ := ctx.Value(licenseContextKey).(*license.License)
return l
}
Gin Middleware
If you use Gin, the middleware pattern adapts cleanly:
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"yourapp/license"
)
// GinRequireLicense validates the license key on every request.
func GinRequireLicense(apiKey string) gin.HandlerFunc {
return func(c *gin.Context) {
key := c.GetHeader("X-License-Key")
if key == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "missing license key",
})
return
}
result, err := license.Validate(c.Request.Context(), apiKey, license.ValidateRequest{
LicenseKey: key,
Domain: c.Request.Host,
})
if err != nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
"error": "validation service unavailable",
})
return
}
if !result.Valid {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "invalid license",
})
return
}
c.Set("license", result.License)
c.Next()
}
}
The pattern for Echo and Fiber is nearly identical — replace the context accessors with each framework's API, but the validation logic stays the same.
gRPC Unary Interceptor
For gRPC services, use a unary server interceptor to validate license keys passed via metadata:
package middleware
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"yourapp/license"
)
// UnaryLicenseInterceptor validates the license key from gRPC metadata.
func UnaryLicenseInterceptor(apiKey string) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
keys := md.Get("x-license-key")
if len(keys) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing license key")
}
result, err := license.Validate(ctx, apiKey, license.ValidateRequest{
LicenseKey: keys[0],
})
if err != nil {
return nil, status.Error(codes.Unavailable, fmt.Sprintf("validation error: %v", err))
}
if !result.Valid {
return nil, status.Error(codes.PermissionDenied, "invalid license")
}
ctx = context.WithValue(ctx, licenseContextKey, result.License)
return handler(ctx, req)
}
}
CLI Flag / Environment Variable
For CLI tools, accept the license key via flag or environment variable:
package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"yourapp/license"
)
func main() {
licenseKey := flag.String("license-key", "", "License key for activation")
flag.Parse()
// Fall back to environment variable
if *licenseKey == "" {
*licenseKey = os.Getenv("APP_LICENSE_KEY")
}
if *licenseKey == "" {
fmt.Fprintln(os.Stderr, "error: license key required (--license-key or APP_LICENSE_KEY)")
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
result, err := license.Validate(ctx, os.Getenv("TO_API_KEY"), license.ValidateRequest{
LicenseKey: *licenseKey,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not validate license: %v
", err)
os.Exit(1)
}
if !result.Valid {
fmt.Fprintf(os.Stderr, "error: invalid license: %s
", result.Error)
os.Exit(1)
}
fmt.Printf("License valid (tier: %s). Starting application...
", result.License.Tier)
// ... proceed with your app logic
}
Offline Validation with Ed25519
Go's standard library includes crypto/ed25519, making offline signature verification trivial with no CGO dependency. This is critical for CLI tools, embedded systems, and any deployment that operates behind firewalls or in air-gapped environments.
Verifying Signed License Tokens
When a license is validated online, Traffic Orchestrator can return a signed token containing the license payload and an Ed25519 signature. Your application stores this token locally and verifies it without any network access:
package license
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"time"
)
// Your Traffic Orchestrator account's Ed25519 public key (from dashboard).
// This is safe to embed in the binary — it can only verify, not sign.
const publicKeyBase64 = "MCowBQYDK2VwAyEA..."
// OfflineToken represents a signed license payload for offline verification.
type OfflineToken struct {
Payload string `json:"payload"`
Signature string `json:"signature"`
}
// OfflinePayload is the decoded content of a signed token.
type OfflinePayload struct {
LicenseID string `json:"licenseId"`
Key string `json:"key"`
Tier string `json:"tier"`
Features []string `json:"features"`
MachineID string `json:"machineId"`
IssuedAt time.Time `json:"issuedAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
// VerifyOffline checks the Ed25519 signature and expiry of an offline token.
func VerifyOffline(token OfflineToken) (*OfflinePayload, error) {
pubKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
if err != nil {
return nil, fmt.Errorf("decode public key: %w", err)
}
pubKey := ed25519.PublicKey(pubKeyBytes)
sigBytes, err := base64.StdEncoding.DecodeString(token.Signature)
if err != nil {
return nil, fmt.Errorf("decode signature: %w", err)
}
payloadBytes := []byte(token.Payload)
if !ed25519.Verify(pubKey, payloadBytes, sigBytes) {
return nil, fmt.Errorf("invalid signature")
}
var payload OfflinePayload
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return nil, fmt.Errorf("decode payload: %w", err)
}
if time.Now().After(payload.ExpiresAt) {
return nil, fmt.Errorf("token expired at %s", payload.ExpiresAt.Format(time.RFC3339))
}
return &payload, nil
}
Caching Validated Tokens with Expiry
Store the token on the filesystem after a successful online validation and fall back to it when the API is unreachable:
package license
import (
"encoding/json"
"os"
"path/filepath"
)
const tokenFilename = ".license-token.json"
// CacheToken writes the offline token to a file in the user's config directory.
func CacheToken(token OfflineToken) error {
dir, err := os.UserConfigDir()
if err != nil {
return err
}
appDir := filepath.Join(dir, "yourapp")
if err := os.MkdirAll(appDir, 0700); err != nil {
return err
}
data, err := json.Marshal(token)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(appDir, tokenFilename), data, 0600)
}
// LoadCachedToken reads the offline token from the cache directory.
func LoadCachedToken() (*OfflineToken, error) {
dir, err := os.UserConfigDir()
if err != nil {
return nil, err
}
data, err := os.ReadFile(filepath.Join(dir, "yourapp", tokenFilename))
if err != nil {
return nil, err
}
var token OfflineToken
if err := json.Unmarshal(data, &token); err != nil {
return nil, err
}
return &token, nil
}
// ValidateWithFallback tries online validation first, then offline.
func ValidateWithFallback(ctx context.Context, apiKey string, req ValidateRequest) (*ValidateResponse, error) {
result, err := Validate(ctx, apiKey, req)
if err == nil {
return result, nil
}
// Online validation failed — try cached offline token.
token, loadErr := LoadCachedToken()
if loadErr != nil {
return nil, fmt.Errorf("online: %w, offline: %w", err, loadErr)
}
payload, verifyErr := VerifyOffline(*token)
if verifyErr != nil {
return nil, fmt.Errorf("online: %w, offline: %w", err, verifyErr)
}
return &ValidateResponse{
Valid: true,
License: &License{
ID: payload.LicenseID,
Key: payload.Key,
Tier: payload.Tier,
Features: payload.Features,
},
}, nil
}
Advanced Patterns
Graceful Degradation with Feature Flags
Instead of a binary valid/invalid check, use the license tier and feature list to control what functionality is available:
package license
// FeatureGate checks whether the validated license includes a specific feature.
func FeatureGate(l *License, feature string) bool {
if l == nil {
return false
}
for _, f := range l.Features {
if f == feature {
return true
}
}
return false
}
// Usage in a handler:
//
// lic := middleware.GetLicense(r.Context())
// if !license.FeatureGate(lic, "advanced-analytics") {
// http.Error(w, "upgrade required", http.StatusPaymentRequired)
// return
// }
Rate-Limited Validation
Calling the validation API on every request adds latency and consumes your validation quota. Cache the result in memory with a TTL:
package license
import (
"context"
"sync"
"time"
)
// CachedValidator wraps the Validate function with an in-memory TTL cache.
type CachedValidator struct {
apiKey string
ttl time.Duration
mu sync.RWMutex
cache map[string]*cachedResult
}
type cachedResult struct {
response *ValidateResponse
expiresAt time.Time
}
// NewCachedValidator creates a validator that caches results for the given TTL.
func NewCachedValidator(apiKey string, ttl time.Duration) *CachedValidator {
return &CachedValidator{
apiKey: apiKey,
ttl: ttl,
cache: make(map[string]*cachedResult),
}
}
// Validate checks the cache first, then calls the API if the cache is stale.
func (cv *CachedValidator) Validate(ctx context.Context, req ValidateRequest) (*ValidateResponse, error) {
cv.mu.RLock()
if cached, ok := cv.cache[req.LicenseKey]; ok && time.Now().Before(cached.expiresAt) {
cv.mu.RUnlock()
return cached.response, nil
}
cv.mu.RUnlock()
result, err := Validate(ctx, cv.apiKey, req)
if err != nil {
return nil, err
}
cv.mu.Lock()
cv.cache[req.LicenseKey] = &cachedResult{
response: result,
expiresAt: time.Now().Add(cv.ttl),
}
cv.mu.Unlock()
return result, nil
}
A 5-minute TTL means each unique license key triggers at most 12 API calls per hour instead of thousands. Adjust the TTL based on your security requirements — shorter for high-value licenses, longer for high-traffic services.
Context-Aware Validation
Go's context.Context carries deadlines, cancellation signals, and request-scoped values. Always pass context through your validation chain so callers can control timeouts and cancellation:
// Use a short timeout for validation in hot paths.
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
result, err := validator.Validate(ctx, license.ValidateRequest{
LicenseKey: key,
})
if err != nil {
// context.DeadlineExceeded means the validation API took too long.
// Decide whether to fail open (allow) or fail closed (deny).
if errors.Is(err, context.DeadlineExceeded) {
// Fail open: use cached result if available
return serveCachedResponse(w, r, key)
}
http.Error(w, "service unavailable", http.StatusServiceUnavailable)
return
}
Building a Go Licensing Package
For anything beyond a prototype, organize your licensing code as a reusable internal package. Here is a recommended structure:
pkg/license/
├── client.go // HTTP client, Validate(), API types
├── offline.go // Ed25519 verification, token caching
├── cache.go // Thread-safe in-memory cache
├── middleware.go // HTTP middleware (net/http, Gin, Echo)
├── interceptor.go // gRPC unary + stream interceptors
├── options.go // Functional options for Client configuration
└── license_test.go // Table-driven tests
Configuration via Functional Options
Use the functional options pattern for a clean, extensible client API:
package license
import (
"net/http"
"time"
)
// Client is a thread-safe license validation client.
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
cacheTTL time.Duration
cache *CachedValidator
}
// Option configures a Client.
type Option func(*Client)
// WithBaseURL overrides the default API base URL.
func WithBaseURL(url string) Option {
return func(c *Client) { c.baseURL = url }
}
// WithHTTPClient sets a custom HTTP client (useful for proxies, custom TLS).
func WithHTTPClient(hc *http.Client) Option {
return func(c *Client) { c.httpClient = hc }
}
// WithCacheTTL sets the in-memory cache duration for validation results.
func WithCacheTTL(ttl time.Duration) Option {
return func(c *Client) { c.cacheTTL = ttl }
}
// NewClient creates a configured license validation client.
func NewClient(apiKey string, opts ...Option) *Client {
c := &Client{
apiKey: apiKey,
baseURL: apiBaseURL,
httpClient: &http.Client{Timeout: 10 * time.Second},
cacheTTL: 5 * time.Minute,
}
for _, opt := range opts {
opt(c)
}
c.cache = NewCachedValidator(c.apiKey, c.cacheTTL)
return c
}
Callers configure the client at initialization:
client := license.NewClient(
os.Getenv("TO_API_KEY"),
license.WithCacheTTL(10*time.Minute),
license.WithBaseURL("https://api.trafficorchestrator.com/api/v3"),
)
Testing License Validation
Go's testing tools make it straightforward to test license validation without hitting a real API.
httptest Server for Mocking
Use net/http/httptest to create a local server that mimics the Traffic Orchestrator API:
package license_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"context"
"yourapp/pkg/license"
)
func TestValidate_ValidKey(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer test-api-key" {
t.Error("missing or incorrect Authorization header")
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(license.ValidateResponse{
Valid: true,
License: &license.License{
ID: "lic_123",
Tier: "professional",
Features: []string{"analytics", "export"},
},
})
}))
defer server.Close()
client := license.NewClient(
"test-api-key",
license.WithBaseURL(server.URL),
)
result, err := client.Validate(context.Background(), license.ValidateRequest{
LicenseKey: "TO-XXXX-XXXX-XXXX",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Valid {
t.Error("expected valid license")
}
if result.License.Tier != "professional" {
t.Errorf("expected tier professional, got %s", result.License.Tier)
}
}
Table-Driven Tests
Test multiple validation scenarios with Go's table-driven test pattern:
func TestValidate_Scenarios(t *testing.T) {
tests := []struct {
name string
key string
serverResp license.ValidateResponse
serverCode int
wantValid bool
wantErr bool
}{
{
name: "valid professional license",
key: "TO-VALID-PRO-KEY",
serverResp: license.ValidateResponse{Valid: true, License: &license.License{Tier: "professional"}},
serverCode: 200,
wantValid: true,
},
{
name: "expired license",
key: "TO-EXPIRED-KEY",
serverResp: license.ValidateResponse{Valid: false, Error: "license_expired"},
serverCode: 200,
wantValid: false,
},
{
name: "revoked license",
key: "TO-REVOKED-KEY",
serverResp: license.ValidateResponse{Valid: false, Error: "license_revoked"},
serverCode: 200,
wantValid: false,
},
{
name: "server error",
key: "TO-ANY-KEY",
serverCode: 500,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.serverCode)
if tt.serverCode == 200 {
json.NewEncoder(w).Encode(tt.serverResp)
}
}))
defer server.Close()
client := license.NewClient("test-key", license.WithBaseURL(server.URL))
result, err := client.Validate(context.Background(), license.ValidateRequest{
LicenseKey: tt.key,
})
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Valid != tt.wantValid {
t.Errorf("valid = %v, want %v", result.Valid, tt.wantValid)
}
})
}
}
Build Tags for License-Gated Features
Use Go build tags to compile different feature sets based on license tier at build time:
// file: features_pro.go
//go:build pro
package features
// ProAnalytics enables the advanced analytics module.
// This file is only included when built with: go build -tags pro
func ProAnalytics() bool { return true }
// file: features_default.go
//go:build !pro
package features
func ProAnalytics() bool { return false }
Build the professional edition with go build -tags pro. The default build excludes pro features entirely — the code is never compiled into the binary, providing compile-time enforcement.
Distribution Considerations
Static Binaries and Embedded Checks
Go's static linking means your license validation code is compiled directly into the binary. There is no separate DLL or shared library to strip out. Embed your public key for offline verification at compile time:
import _ "embed"
//go:embed keys/ed25519-public.pem
var publicKeyPEM []byte
The key is baked into the binary at compile time. Users cannot replace it without recompiling, which they cannot do without your source code.
CGO-Free Validation
Go's crypto/ed25519 is a pure Go implementation — no CGO required. This means your offline validation works on every platform Go supports, including architectures without a C toolchain:
# Build for every major platform — all with license validation
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/myapp-linux-amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/myapp-linux-arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dist/myapp-darwin-arm64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/myapp-windows-amd64.exe
Every binary includes the same license validation logic, Ed25519 verification, and offline fallback. No platform-specific code, no runtime dependencies, no dynamic linking.
Docker Multi-Stage Builds
For containerized deployments, use a multi-stage build that validates the license at build time (for CI/CD pipelines) and embeds runtime validation:
# Stage 1: Build
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Embed the license public key and build a static binary
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/server ./cmd/server
# Stage 2: Runtime (distroless for minimal attack surface)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
# License key injected at runtime via environment variable
ENV APP_LICENSE_KEY=""
ENTRYPOINT ["/server"]
The license key is injected at runtime via environment variable or Kubernetes secret — never baked into the image. The binary validates the key on startup using the embedded public key for offline verification or the API for online validation.
| Approach | When to Use | Security Level |
|---|---|---|
| Environment variable | Container orchestrators, CI/CD | Medium — readable by anyone with container access |
| Kubernetes Secret | Production Kubernetes clusters | High — encrypted at rest, RBAC-controlled |
| HashiCorp Vault | Enterprise, regulated environments | Highest — dynamic secrets, audit logging |
| Embedded at build time | Appliance binaries, air-gapped | Medium — key travels with the binary |
Putting It All Together
Here is the complete validation flow for a production Go application:
- Initialize the client — create a
license.Clientwith functional options at startup. Set the cache TTL based on your security requirements. - Attach middleware — use the HTTP middleware or gRPC interceptor to validate the license on every request. The middleware caches results in memory via
CachedValidator. - Feature gating — use the license tier and feature list from the validation response to control which endpoints and functionality are available.
- Offline fallback — when the API is unreachable, verify the cached Ed25519-signed token locally. Pure Go, no CGO, works on every platform.
- Graceful degradation — if both online and offline validation fail, enter a degraded mode that preserves core functionality while disabling premium features.
- Testing — use
httptestservers and table-driven tests to cover every validation scenario without hitting the real API.
The entire pkg/license/ package compiles to roughly 50 KB of additional binary size. It has zero external dependencies beyond Go's standard library for the core validation path (only the HTTP framework middleware imports framework-specific packages). It cross-compiles to every platform Go supports, runs without CGO, and handles offline scenarios with cryptographic integrity.
License Your Go Application Today
Traffic Orchestrator provides the validation API, Ed25519 offline tokens, domain binding, and analytics — you write the Go code. The free Builder plan includes 5 licenses, 500 validations/month, and full API access with no credit card required.
Create Free AccountShip licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.